Compare commits

..

22 Commits

Author SHA1 Message Date
Zach e53c493e78 update TS 2026-04-23 18:01:35 -07:00
Zach 54e0dae172 Add placeholder for multi-ACKs setting in localization 2026-04-23 17:58:40 -07:00
Zach 066aba7c5d #401 Refactor multi-ACK localization strings and settings UI
- Updated localization files for multiple languages to change the representation of multi-ACK settings from a string with a placeholder to a simple string.
- Removed unnecessary placeholder definitions for multi-ACK in localization files.
- Adjusted the settings screen to replace the slider for multi-ACK with a switch, simplifying the user interface.
- Updated the Podfile.lock to remove the wakelock_plus dependency.
2026-04-23 17:58:15 -07:00
zjs81 6b6a881c7a Merge pull request #388 from zjs81/msg-chars
add byte counted text input
2026-04-20 09:17:00 -07:00
ericz 8ef8a38495 change to prepare Outbound Text Functions. 2026-04-17 18:32:14 -07:00
Enot (ded) Skelly ddcda4ba5a keep multiline editing 2026-04-17 14:07:00 -07:00
ericz b572314ae9 respect smaz encoding in message byte length calculation. 2026-04-15 09:04:08 -07:00
Enot (ded) Skelly e97fb9bd24 add byte counted text input
adds a new widget that counts bytes during entry

configurable limit and shows user both count and limit

provides color feedback

use new widget in chat and channel text entry
2026-04-15 09:04:08 -07:00
zjs81 a4bbeffddc Merge pull request #386 from zjs81/dev_translations
Dev translations
2026-04-14 21:38:52 -07:00
zjs81 37ec8f2f05 Add localization for chat and repeater features in multiple languages
- Added translations for "Send message", "Guest information", and "Guest tools" in Bulgarian, German, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated the "Clock synchronization after login" feature subtitle in all affected languages.
- Removed untranslated keys from the untranslated.json file as they have now been localized.
2026-04-14 21:38:12 -07:00
zjs81 39cd6d5514 Merge pull request #385 from zjs81/dev
merge dev to main
2026-04-14 21:04:04 -07:00
zjs81 44eb4fad58 Merge pull request #361 from zjs81/unused-plugin
remove unused macos path_provider_foundation
2026-04-14 21:02:30 -07:00
zjs81 1a209cbcfc Merge pull request #372 from zjs81/group-elem
fix: settings dialog lists
2026-04-14 21:02:08 -07:00
zjs81 33a8f34463 Merge pull request #365 from zjs81/rpt-guest
enh: make repeater admin guest aware
2026-04-14 20:44:14 -07:00
zjs81 ce8e8f0d5b Merge pull request #384 from zjs81/clear_toast
clear toast on tap
2026-04-14 20:42:23 -07:00
Enot (ded) Skelly aa2d0f1927 clear toast on tap
this adds a generator showDismissibleSnackBar which by default allows
tapping to clear snack bar toasts. all SnackBar properties are still
available and the

all callers should now use showDismissibleSnackBar() instead of calling
ScaffoldMessenger.of(context).showSnackBar(SnackBar())
2026-04-14 12:01:42 -07:00
Ded 0757c8e53a Merge pull request #369 from just-stuff-tm/auto-time-sync-349
add auto clock synchronization setting after repeater login
2026-04-13 08:20:01 -07:00
Enot (ded) Skelly add4731d05 fix: settings dialog lists
switched to using RadioListTile instead of ListTile to be more accessible
2026-04-10 15:11:44 -07:00
Enot (ded) Skelly 7dc162d968 temp
translations fix
2026-04-10 14:15:14 -07:00
just-stuff-tm 8ba4bbfbc5 add auto clock synchronization setting after repeater login
Introduced a new setting for automatic clock synchronization after a successful repeater login.
Added localization support for the new feature in multiple languages (Bulgarian, German, English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, Chinese).
Implemented storage service methods to manage the new setting.
Updated the repeater settings screen to include a toggle for the new feature.
Enhanced the repeater login dialog to trigger clock synchronization automatically if the setting is enabled.
2026-04-10 14:25:53 -04:00
Ded cac6abfef1 Fix dev
rebase dev over main and resolve merge conflicts
2026-04-09 10:12:47 -07:00
Enot (ded) Skelly bdd7fc0cdd remove unused macos path_provider_foundation
added in #299 but appears not needed, flutter removes when building
2026-04-08 14:56:34 -07:00
76 changed files with 2132 additions and 2426 deletions
+11 -77
View File
@@ -40,7 +40,6 @@ import '../storage/contact_settings_store.dart';
import '../storage/contact_store.dart'; import '../storage/contact_store.dart';
import '../storage/message_store.dart'; import '../storage/message_store.dart';
import '../storage/unread_store.dart'; import '../storage/unread_store.dart';
import '../storage/last_device_store.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
@@ -282,7 +281,6 @@ class MeshCoreConnector extends ChangeNotifier {
final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore(); final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore();
final ChannelStore _channelStore = ChannelStore(); final ChannelStore _channelStore = ChannelStore();
final UnreadStore _unreadStore = UnreadStore(); final UnreadStore _unreadStore = UnreadStore();
final LastDeviceStore _lastDeviceStore = LastDeviceStore();
List<Channel> _cachedChannels = []; List<Channel> _cachedChannels = [];
final Map<int, bool> _channelSmazEnabled = {}; final Map<int, bool> _channelSmazEnabled = {};
bool _lastSentWasCliCommand = bool _lastSentWasCliCommand =
@@ -770,10 +768,6 @@ class MeshCoreConnector extends ChangeNotifier {
_appDebugLogService = appDebugLogService; _appDebugLogService = appDebugLogService;
_backgroundService = backgroundService; _backgroundService = backgroundService;
_timeoutPredictionService = timeoutPredictionService; _timeoutPredictionService = timeoutPredictionService;
// When the app resumes from background, check if we need to reconnect.
_backgroundService?.onResume = _onAppResumed;
_usbManager.setDebugLogService(_appDebugLogService); _usbManager.setDebugLogService(_appDebugLogService);
_tcpConnector.setDebugLogService(_appDebugLogService); _tcpConnector.setDebugLogService(_appDebugLogService);
@@ -1885,7 +1879,6 @@ class MeshCoreConnector extends ChangeNotifier {
); );
_setState(MeshCoreConnectionState.connected); _setState(MeshCoreConnectionState.connected);
_lastDeviceStore.persistLastDevice(_deviceId!, _deviceDisplayName!);
if (_shouldGateInitialChannelSync) { if (_shouldGateInitialChannelSync) {
_hasReceivedDeviceInfo = false; _hasReceivedDeviceInfo = false;
_pendingInitialChannelSync = true; _pendingInitialChannelSync = true;
@@ -2232,56 +2225,6 @@ class MeshCoreConnector extends ChangeNotifier {
}); });
} }
/// Called by [BackgroundService] when the app returns to the foreground.
/// If the BLE connection was lost while backgrounded, this kicks off an
/// immediate reconnect attempt instead of waiting for the next timer tick.
void _onAppResumed() {
if (_shouldAutoReconnect &&
_state != MeshCoreConnectionState.connected &&
_state != MeshCoreConnectionState.connecting) {
_appDebugLogService?.info(
'App resumed triggering reconnect check',
tag: 'Lifecycle',
);
_cancelReconnectTimer();
_scheduleReconnect();
} else if (_state == MeshCoreConnectionState.disconnected &&
_lastDeviceId == null) {
// App was fully restarted (swiped away). Try to restore from prefs.
tryAutoReconnect();
}
}
/// Attempt to reconnect to the last persisted BLE device.
///
/// Called on fresh app start (after a swipe-away kill) so the user is
/// brought straight back to the connected state instead of the scan screen.
Future<bool> tryAutoReconnect() async {
if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) {
return false;
}
final deviceId = _lastDeviceStore.getPersistedDeviceId();
if (deviceId!.isEmpty) {
return false;
}
final displayName = _lastDeviceStore.getPersistedDeviceName();
_appDebugLogService?.info(
'Auto-reconnecting to $deviceId ($displayName)',
tag: 'Lifecycle',
);
try {
final device = BluetoothDevice.fromId(deviceId);
await connect(device, displayName: displayName);
return true;
} catch (e) {
_appDebugLogService?.error('Auto-reconnect failed: $e', tag: 'Lifecycle');
return false;
}
}
Future<void> disconnect({ Future<void> disconnect({
bool manual = true, bool manual = true,
bool skipBleDeviceDisconnect = false, bool skipBleDeviceDisconnect = false,
@@ -2302,8 +2245,6 @@ class MeshCoreConnector extends ChangeNotifier {
if (manual) { if (manual) {
_manualDisconnect = true; _manualDisconnect = true;
_cancelReconnectTimer(); _cancelReconnectTimer();
_lastDeviceStore.clearPersistedDevice();
_notificationService.cancelAll();
unawaited(_backgroundService?.stop()); unawaited(_backgroundService?.stop());
} else { } else {
_manualDisconnect = false; _manualDisconnect = false;
@@ -3053,13 +2994,7 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingChannelSentQueue.add(message.messageId); _pendingChannelSentQueue.add(message.messageId);
notifyListeners(); notifyListeners();
final trimmed = text.trim(); final outboundText = prepareChannelOutboundText(channel.index, text);
final isStructuredPayload =
trimmed.startsWith('g:') || trimmed.startsWith('m:');
final outboundText =
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
? Smaz.encodeIfSmaller(text)
: text;
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime); await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame( await sendFrame(
buildSendChannelTextMsgFrame(channel.index, outboundText), buildSendChannelTextMsgFrame(channel.index, outboundText),
@@ -4511,6 +4446,16 @@ class MeshCoreConnector extends ChangeNotifier {
return text; return text;
} }
String prepareChannelOutboundText(int channelIndex, String text) {
final trimmed = text.trim();
final isStructuredPayload =
trimmed.startsWith('g:') || trimmed.startsWith('m:');
if (!isStructuredPayload && isChannelSmazEnabled(channelIndex)) {
return Smaz.encodeIfSmaller(text);
}
return text;
}
String _channelDisplayName(int channelIndex) { String _channelDisplayName(int channelIndex) {
for (final channel in _channels) { for (final channel in _channels) {
if (channel.index != channelIndex) continue; if (channel.index != channelIndex) continue;
@@ -4969,17 +4914,6 @@ class MeshCoreConnector extends ChangeNotifier {
); );
} }
/// Public accessor to find a channel by its index.
Channel? findChannelByIndex(int index) => _findChannelByIndex(index);
/// Find a contact by its public key hex string.
Contact? findContactByKeyHex(String keyHex) {
return _contacts.cast<Contact?>().firstWhere(
(c) => c?.publicKeyHex == keyHex,
orElse: () => null,
);
}
void _maybeIncrementChannelUnread( void _maybeIncrementChannelUnread(
ChannelMessage message, { ChannelMessage message, {
required bool isNew, required bool isNew,
+5 -6
View File
@@ -3,6 +3,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../helpers/snack_bar_builder.dart';
class LinkHandler { class LinkHandler {
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) { static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
@@ -93,21 +94,19 @@ class LinkHandler {
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_couldNotOpenLink(url)), content: Text(context.l10n.chat_couldNotOpenLink(url)),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_invalidLink), content: Text(context.l10n.chat_invalidLink),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss
// all other properties are default and optional
void showDismissibleSnackBar(
BuildContext context, {
Key? key,
required Widget content,
Color? backgroundColor,
double? elevation,
EdgeInsetsGeometry? margin,
EdgeInsetsGeometry? padding,
double? width,
ShapeBorder? shape,
HitTestBehavior? hitTestBehavior,
SnackBarBehavior? behavior,
SnackBarAction? action,
double? actionOverflowThreshold,
bool? showCloseIcon,
Color? closeIconColor,
Duration? duration,
bool? persist,
Animation<double>? animation,
void Function()? onVisible,
DismissDirection? dismissDirection,
Clip? clipBehavior,
}) {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
SnackBar(
key: key,
content: GestureDetector(
onTap: () => messenger.hideCurrentSnackBar(),
child: content,
),
backgroundColor: backgroundColor,
elevation: elevation,
margin: margin,
padding: padding,
width: width,
shape: shape,
hitTestBehavior: hitTestBehavior,
behavior: behavior,
action: action,
actionOverflowThreshold: actionOverflowThreshold,
showCloseIcon: showCloseIcon,
closeIconColor: closeIconColor,
duration: duration ?? const Duration(seconds: 4),
persist: persist,
animation: animation,
onVisible: onVisible,
dismissDirection: dismissDirection ?? DismissDirection.down,
clipBehavior: clipBehavior ?? Clip.hardEdge,
),
);
}
+16 -3
View File
@@ -4,8 +4,14 @@ import 'package:flutter/services.dart';
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
final int maxBytes; final int maxBytes;
final String Function(String)? encoder;
const Utf8LengthLimitingTextInputFormatter(this.maxBytes); const Utf8LengthLimitingTextInputFormatter(this.maxBytes, {this.encoder});
int _effectiveByteLength(String text) {
final effective = encoder != null ? encoder!(text) : text;
return utf8.encode(effective).length;
}
@override @override
TextEditingValue formatEditUpdate( TextEditingValue formatEditUpdate(
@@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
TextEditingValue newValue, TextEditingValue newValue,
) { ) {
if (maxBytes <= 0) return oldValue; if (maxBytes <= 0) return oldValue;
final bytes = utf8.encode(newValue.text); if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue;
if (bytes.length <= maxBytes) return newValue;
final truncated = _truncateToMaxBytes(newValue.text, maxBytes); final truncated = _truncateToMaxBytes(newValue.text, maxBytes);
return TextEditingValue( return TextEditingValue(
@@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
} }
String _truncateToMaxBytes(String text, int limit) { String _truncateToMaxBytes(String text, int limit) {
if (encoder != null) {
final runes = text.runes.toList();
while (runes.isNotEmpty &&
_effectiveByteLength(String.fromCharCodes(runes)) > maxBytes) {
runes.removeLast();
}
return String.fromCharCodes(runes);
}
final buffer = StringBuffer(); final buffer = StringBuffer();
var used = 0; var used = 0;
for (final rune in text.runes) { for (final rune in text.runes) {
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение", "contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
"contact_teleLoc": "Местоположение на телеметрията", "contact_teleLoc": "Местоположение на телеметрията",
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици", "contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута", "appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута", "appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути", "appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
@@ -1940,7 +1933,6 @@
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение", "appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.", "appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Мулти-потвърди: {value}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен", "settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
"map_showOverlaps": "Покриване на ключа на повтаряча", "map_showOverlaps": "Покриване на ключа на повтаряча",
"map_runTraceWithReturnPath": "Върни се по същия път.", "map_runTraceWithReturnPath": "Върни се по същия път.",
@@ -2061,5 +2053,18 @@
"scanner_linuxPairingHidePin": "Скриване на PIN кода", "scanner_linuxPairingHidePin": "Скриване на PIN кода",
"scanner_linuxPairingShowPin": "Покажи PIN", "scanner_linuxPairingShowPin": "Покажи PIN",
"repeater_cliQuickClockSync": "Синхронизация на часовника", "repeater_cliQuickClockSync": "Синхронизация на часовника",
"repeater_cliQuickDiscovery": "Открий Съседи" "repeater_cliQuickDiscovery": "Открий Съседи",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.",
"repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане",
"chat_sendMessage": "Изпратете съобщение",
"room_guest": "Информация за сървъра на стаята",
"repeater_guest": "Информация за ретранслаторите",
"repeater_guestTools": "Инструменти за гости",
"settings_multiAck": "Множество потвърждения"
} }
+14 -9
View File
@@ -1950,13 +1950,6 @@
"contact_lastSeen": "Zuletzt gesehen", "contact_lastSeen": "Zuletzt gesehen",
"contact_clearChat": "Chat löschen", "contact_clearChat": "Chat löschen",
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen", "contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade", "appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
"appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.", "appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
"appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge", "appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
@@ -1969,7 +1962,6 @@
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.", "appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert", "settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
"map_showOverlaps": "Überlappungen der Repeater-Taste", "map_showOverlaps": "Überlappungen der Repeater-Taste",
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.", "map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2089,5 +2081,18 @@
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).", "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).",
"repeater_cliQuickClockSync": "Uhr Synchronisieren", "repeater_cliQuickClockSync": "Uhr Synchronisieren",
"repeater_cliQuickDiscovery": "Entdecke Nachbarn" "repeater_cliQuickDiscovery": "Entdecke Nachbarn",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Uhrzeit-Synchronisation nach dem Anmelden",
"repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.",
"repeater_guest": "Informationen zu Repeatern",
"repeater_guestTools": "Gastwerkzeuge",
"chat_sendMessage": "Nachricht senden",
"room_guest": "Informationen zum Room Server",
"settings_multiAck": "Mehrere Bestätigungen"
} }
+14 -10
View File
@@ -178,14 +178,7 @@
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode", "settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location", "settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.", "settings_advertLocationSubtitle": "Include location in advert.",
"settings_multiAck": "Multi-ACKs: {value}", "settings_multiAck": "Multi-ACKs",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Telemetry mode updated", "settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions", "settings_actions": "Actions",
"settings_sendAdvertisement": "Send Advertisement", "settings_sendAdvertisement": "Send Advertisement",
@@ -1038,8 +1031,8 @@
"login_enterPassword": "Enter password", "login_enterPassword": "Enter password",
"login_savePassword": "Save password", "login_savePassword": "Save password",
"login_savePasswordSubtitle": "Password will be stored securely on this device", "login_savePasswordSubtitle": "Password will be stored securely on this device",
"login_repeaterDescription": "Enter the repeater password to access settings and status.", "login_repeaterDescription": "Enter the repeater password for guest or admin access.",
"login_roomDescription": "Enter the room password to access settings and status.", "login_roomDescription": "Enter the room password for guest or admin access.",
"login_routing": "Routing", "login_routing": "Routing",
"login_routingMode": "Routing mode", "login_routingMode": "Routing mode",
"login_autoUseSavedPath": "Auto (use saved path)", "login_autoUseSavedPath": "Auto (use saved path)",
@@ -1105,7 +1098,10 @@
"path_setPath": "Set Path", "path_setPath": "Set Path",
"repeater_management": "Repeater Management", "repeater_management": "Repeater Management",
"room_management": "Room Server Management", "room_management": "Room Server Management",
"repeater_guest": "Repeater Information",
"room_guest": "Room Server Information",
"repeater_managementTools": "Management Tools", "repeater_managementTools": "Management Tools",
"repeater_guestTools": "Guest Tools",
"repeater_status": "Status", "repeater_status": "Status",
"repeater_statusSubtitle": "View repeater status, stats, and neighbors", "repeater_statusSubtitle": "View repeater status, stats, and neighbors",
"repeater_telemetry": "Telemetry", "repeater_telemetry": "Telemetry",
@@ -1116,6 +1112,14 @@
"repeater_neighborsSubtitle": "View zero hop neighbors.", "repeater_neighborsSubtitle": "View zero hop neighbors.",
"repeater_settings": "Settings", "repeater_settings": "Settings",
"repeater_settingsSubtitle": "Configure repeater parameters", "repeater_settingsSubtitle": "Configure repeater parameters",
"repeater_clockSyncAfterLogin": "Clock sync after login",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatically send \"clock sync\" after a successful login",
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_statusTitle": "Repeater Status", "repeater_statusTitle": "Repeater Status",
"repeater_routingMode": "Routing mode", "repeater_routingMode": "Routing mode",
"repeater_autoUseSavedPath": "Auto (use saved path)", "repeater_autoUseSavedPath": "Auto (use saved path)",
+14 -9
View File
@@ -1950,13 +1950,6 @@
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica", "contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
"contact_teleEnv": "Entorno de Telemetría", "contact_teleEnv": "Entorno de Telemetría",
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno", "contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Peso inicial de la ruta", "appSettings_initialRouteWeight": "Peso inicial de la ruta",
"appSettings_maxRouteWeight": "Peso máximo permitido para la ruta", "appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas", "appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
@@ -1969,7 +1962,6 @@
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.", "appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetría actualizado", "settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Superposiciones de tecla repetidora", "map_showOverlaps": "Superposiciones de tecla repetidora",
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino.", "map_runTraceWithReturnPath": "Volver atrás por el mismo camino.",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2089,5 +2081,18 @@
"translation_translationOptions": "Opciones de traducción", "translation_translationOptions": "Opciones de traducción",
"translation_systemLanguage": "Idioma del sistema", "translation_systemLanguage": "Idioma del sistema",
"repeater_cliQuickDiscovery": "Descubrir Vecinos", "repeater_cliQuickDiscovery": "Descubrir Vecinos",
"repeater_cliQuickClockSync": "Sincronización del reloj" "repeater_cliQuickClockSync": "Sincronización del reloj",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.",
"repeater_clockSyncAfterLogin": "Sincronización del reloj después de iniciar sesión",
"repeater_guest": "Información sobre repetidores",
"chat_sendMessage": "Enviar mensaje",
"repeater_guestTools": "Herramientas para invitados",
"room_guest": "Información del servidor",
"settings_multiAck": "Múltiples respuestas de confirmación"
} }
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Dernière fois vu", "contact_lastSeen": "Dernière fois vu",
"contact_clearChat": "Effacer la conversation", "contact_clearChat": "Effacer la conversation",
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base", "contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.", "appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.",
"appSettings_initialRouteWeight": "Poids initial de l'itinéraire", "appSettings_initialRouteWeight": "Poids initial de l'itinéraire",
"appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet", "appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
@@ -1940,7 +1933,6 @@
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages", "appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.", "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Multi-ACKs : {value}",
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour", "settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
"map_showOverlaps": "Chevauchement de la touche répétitive", "map_showOverlaps": "Chevauchement de la touche répétitive",
"map_runTraceWithReturnPath": "Revenir sur le même chemin.", "map_runTraceWithReturnPath": "Revenir sur le même chemin.",
@@ -2061,5 +2053,18 @@
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).", "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
"scanner_linuxPairingShowPin": "Afficher le code PIN", "scanner_linuxPairingShowPin": "Afficher le code PIN",
"repeater_cliQuickClockSync": "Synchronisation de l'horloge", "repeater_cliQuickClockSync": "Synchronisation de l'horloge",
"repeater_cliQuickDiscovery": "Découvrir les voisins" "repeater_cliQuickDiscovery": "Découvrir les voisins",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Envoyer automatiquement une notification \"synchronisation de l'heure\" après une connexion réussie.",
"repeater_clockSyncAfterLogin": "Synchronisation de l'horloge après la connexion",
"repeater_guestTools": "Outils pour les invités",
"chat_sendMessage": "Envoyer un message",
"room_guest": "Informations sur le serveur",
"repeater_guest": "Informations sur les répéteurs",
"settings_multiAck": "Plusieurs accusés de réception"
} }
+15 -11
View File
@@ -2012,13 +2012,6 @@
"radioStats_stripWaiting": "Rádió adatok begyűjtése…", "radioStats_stripWaiting": "Rádió adatok begyűjtése…",
"radioStats_settingsTile": "Rádió statisztikák", "radioStats_settingsTile": "Rádió statisztikák",
"radioStats_settingsSubtitle": "Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama", "radioStats_settingsSubtitle": "Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_denyAll": "Elutasítom", "settings_denyAll": "Elutasítom",
"settings_privacySettingsDescription": "Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.", "settings_privacySettingsDescription": "Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.",
"settings_privacySubtitle": "Ellenőrizd, hogy milyen információkat osztanak meg.", "settings_privacySubtitle": "Ellenőrizd, hogy milyen információkat osztanak meg.",
@@ -2030,7 +2023,6 @@
"settings_telemetryEnvironmentMode": "Adatkapcsolati környezeti mód", "settings_telemetryEnvironmentMode": "Adatkapcsolati környezeti mód",
"settings_advertLocation": "Reklám megjelenési hely", "settings_advertLocation": "Reklám megjelenési hely",
"settings_advertLocationSubtitle": "A hirdetés tartalmazza a helyszínt.", "settings_advertLocationSubtitle": "A hirdetés tartalmazza a helyszínt.",
"settings_multiAck": "Többszöri visszaigazolások: {value}",
"settings_telemetryModeUpdated": "A telemetriamód frissítve", "settings_telemetryModeUpdated": "A telemetriamód frissítve",
"contact_info": "Kapcsolattartási információk", "contact_info": "Kapcsolattartási információk",
"contact_settings": "Kapcsolat beállítások", "contact_settings": "Kapcsolat beállítások",
@@ -2081,7 +2073,7 @@
} }
}, },
"scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot", "scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot",
"scanner_linuxPairingPinPrompt": "Adja meg a PIN kódot a {deviceName} számára (hagyja üresen, ha nincs).", "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).",
"scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot", "scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot",
"scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN",
"@translation_translateTo": { "@translation_translateTo": {
@@ -2098,7 +2090,19 @@
"translation_translateTo": "Fordítás {language}-ra", "translation_translateTo": "Fordítás {language}-ra",
"translation_translationOptions": "Fordítási lehetőségek", "translation_translationOptions": "Fordítási lehetőségek",
"translation_systemLanguage": "Rendszer nyelvé", "translation_systemLanguage": "Rendszer nyelvé",
"scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).",
"repeater_cliQuickClockSync": "Óra szinkronizálás", "repeater_cliQuickClockSync": "Óra szinkronizálás",
"repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat" "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.",
"repeater_clockSyncAfterLogin": "Óra szinkronizálás bejelentkezés után",
"repeater_guestTools": "Vendégek számára elérhető eszközök",
"room_guest": "Szoba szerver információk",
"chat_sendMessage": "Üzenet küldése",
"repeater_guest": "Adatok a repeaterről",
"settings_multiAck": "Többszörös visszaigazolások"
} }
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base", "contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale", "contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
"contact_teleEnv": "Ambiente di telemetria", "contact_teleEnv": "Ambiente di telemetria",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Peso iniziale del percorso", "appSettings_initialRouteWeight": "Peso iniziale del percorso",
"appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi", "appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
"appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.", "appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
@@ -1941,7 +1934,6 @@
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.", "appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata", "settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore", "map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso", "map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2061,5 +2053,18 @@
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth", "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth",
"scanner_linuxPairingHidePin": "Nascondi il PIN", "scanner_linuxPairingHidePin": "Nascondi il PIN",
"repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
"repeater_cliQuickDiscovery": "Scopri i Vicini" "repeater_cliQuickDiscovery": "Scopri i Vicini",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Invia automaticamente il comando \"sincronizzazione dell'orologio\" dopo un login riuscito.",
"repeater_clockSyncAfterLogin": "Sincronizzazione dell'orologio dopo il login",
"repeater_guest": "Informazioni sul ripetitore",
"repeater_guestTools": "Strumenti per gli ospiti",
"chat_sendMessage": "Invia messaggio",
"room_guest": "Informazioni sul server",
"settings_multiAck": "ACK multipli"
} }
+14 -9
View File
@@ -2012,13 +2012,6 @@
"radioStats_stripWaiting": "ラジオの統計情報を取得中…", "radioStats_stripWaiting": "ラジオの統計情報を取得中…",
"radioStats_settingsTile": "ラジオの統計", "radioStats_settingsTile": "ラジオの統計",
"radioStats_settingsSubtitle": "ノイズレベル、RSSI、SNR、および通信時間", "radioStats_settingsSubtitle": "ノイズレベル、RSSI、SNR、および通信時間",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_privacy": "プライバシー設定", "settings_privacy": "プライバシー設定",
"settings_privacySubtitle": "共有する情報の内容を管理する。", "settings_privacySubtitle": "共有する情報の内容を管理する。",
"settings_denyAll": "すべてを否定", "settings_denyAll": "すべてを否定",
@@ -2030,7 +2023,6 @@
"settings_telemetryEnvironmentMode": "テレメトリ環境モード", "settings_telemetryEnvironmentMode": "テレメトリ環境モード",
"settings_advertLocation": "広告掲載場所", "settings_advertLocation": "広告掲載場所",
"settings_advertLocationSubtitle": "広告に場所を記載してください。", "settings_advertLocationSubtitle": "広告に場所を記載してください。",
"settings_multiAck": "複数のACK{value}",
"settings_telemetryModeUpdated": "テレメトリモードが更新されました", "settings_telemetryModeUpdated": "テレメトリモードが更新されました",
"contact_info": "連絡先", "contact_info": "連絡先",
"contact_settings": "連絡設定", "contact_settings": "連絡設定",
@@ -2099,5 +2091,18 @@
"scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN",
"scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。",
"repeater_cliQuickClockSync": "クロック同期", "repeater_cliQuickClockSync": "クロック同期",
"repeater_cliQuickDiscovery": "近隣を発見する" "repeater_cliQuickDiscovery": "近隣を発見する",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "ログイン後、時計の時刻を同期する",
"repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。",
"room_guest": "ルームサーバーに関する情報",
"chat_sendMessage": "メッセージを送信する",
"repeater_guest": "繰り返し送信に関する情報",
"repeater_guestTools": "ゲスト向けツール",
"settings_multiAck": "複数のACK(応答)"
} }
+14 -9
View File
@@ -2012,13 +2012,6 @@
"radioStats_stripWaiting": "라디오 통계 가져오기…", "radioStats_stripWaiting": "라디오 통계 가져오기…",
"radioStats_settingsTile": "라디오 통계", "radioStats_settingsTile": "라디오 통계",
"radioStats_settingsSubtitle": "잡음 수준, RSSI, 신호 대 잡음비, 통신 시간", "radioStats_settingsSubtitle": "잡음 수준, RSSI, 신호 대 잡음비, 통신 시간",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_privacy": "개인 정보 설정", "settings_privacy": "개인 정보 설정",
"settings_privacySubtitle": "어떤 정보를 공유할지 통제하세요.", "settings_privacySubtitle": "어떤 정보를 공유할지 통제하세요.",
"settings_privacySettingsDescription": "어떤 정보를 기기가 다른 사람들과 공유할지 선택하세요.", "settings_privacySettingsDescription": "어떤 정보를 기기가 다른 사람들과 공유할지 선택하세요.",
@@ -2030,7 +2023,6 @@
"settings_telemetryEnvironmentMode": "텔레메트리 환경 모드", "settings_telemetryEnvironmentMode": "텔레메트리 환경 모드",
"settings_advertLocation": "광고 위치", "settings_advertLocation": "광고 위치",
"settings_advertLocationSubtitle": "광고에 위치 정보를 포함하세요.", "settings_advertLocationSubtitle": "광고에 위치 정보를 포함하세요.",
"settings_multiAck": "다중 ACK: {value}",
"settings_telemetryModeUpdated": "텔레메트리 모드 업데이트 완료", "settings_telemetryModeUpdated": "텔레메트리 모드 업데이트 완료",
"contact_info": "연락처", "contact_info": "연락처",
"contact_settings": "연락처 설정", "contact_settings": "연락처 설정",
@@ -2099,5 +2091,18 @@
"translation_translationOptions": "번역 옵션", "translation_translationOptions": "번역 옵션",
"translation_systemLanguage": "시스템 언어", "translation_systemLanguage": "시스템 언어",
"repeater_cliQuickClockSync": "시계 동기화", "repeater_cliQuickClockSync": "시계 동기화",
"repeater_cliQuickDiscovery": "이웃 발견하기" "repeater_cliQuickDiscovery": "이웃 발견하기",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "로그인 후 시계 동기화",
"repeater_clockSyncAfterLoginSubtitle": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.",
"repeater_guestTools": "손님용 도구",
"chat_sendMessage": "메시지를 보내기",
"repeater_guest": "반복 장비 정보",
"room_guest": "서버 정보",
"settings_multiAck": "다중 ACK"
} }
+34 -4
View File
@@ -901,8 +901,8 @@ abstract class AppLocalizations {
/// No description provided for @settings_multiAck. /// No description provided for @settings_multiAck.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Multi-ACKs: {value}'** /// **'Multi-ACKs'**
String settings_multiAck(String value); String get settings_multiAck;
/// No description provided for @settings_telemetryModeUpdated. /// No description provided for @settings_telemetryModeUpdated.
/// ///
@@ -3438,13 +3438,13 @@ abstract class AppLocalizations {
/// No description provided for @login_repeaterDescription. /// No description provided for @login_repeaterDescription.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Enter the repeater password to access settings and status.'** /// **'Enter the repeater password for guest or admin access.'**
String get login_repeaterDescription; String get login_repeaterDescription;
/// No description provided for @login_roomDescription. /// No description provided for @login_roomDescription.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Enter the room password to access settings and status.'** /// **'Enter the room password for guest or admin access.'**
String get login_roomDescription; String get login_roomDescription;
/// No description provided for @login_routing. /// No description provided for @login_routing.
@@ -3609,12 +3609,30 @@ abstract class AppLocalizations {
/// **'Room Server Management'** /// **'Room Server Management'**
String get room_management; String get room_management;
/// No description provided for @repeater_guest.
///
/// In en, this message translates to:
/// **'Repeater Information'**
String get repeater_guest;
/// No description provided for @room_guest.
///
/// In en, this message translates to:
/// **'Room Server Information'**
String get room_guest;
/// No description provided for @repeater_managementTools. /// No description provided for @repeater_managementTools.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Management Tools'** /// **'Management Tools'**
String get repeater_managementTools; String get repeater_managementTools;
/// No description provided for @repeater_guestTools.
///
/// In en, this message translates to:
/// **'Guest Tools'**
String get repeater_guestTools;
/// No description provided for @repeater_status. /// No description provided for @repeater_status.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -3675,6 +3693,18 @@ abstract class AppLocalizations {
/// **'Configure repeater parameters'** /// **'Configure repeater parameters'**
String get repeater_settingsSubtitle; String get repeater_settingsSubtitle;
/// Repeater setting: auto sync device clock after successful login
///
/// In en, this message translates to:
/// **'Clock sync after login'**
String get repeater_clockSyncAfterLogin;
/// Repeater setting subtitle: describes the clock sync after login behavior
///
/// In en, this message translates to:
/// **'Automatically send \"clock sync\" after a successful login'**
String get repeater_clockSyncAfterLoginSubtitle;
/// No description provided for @repeater_statusTitle. /// No description provided for @repeater_statusTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+19 -4
View File
@@ -437,9 +437,7 @@ class AppLocalizationsBg extends AppLocalizations {
'Включи местоположение в обявата'; 'Включи местоположение в обявата';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Множество потвърждения';
return 'Мулти-потвърди: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен'; String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
@@ -1240,7 +1238,7 @@ class AppLocalizationsBg extends AppLocalizations {
String get chat_noMessages => 'Няма съобщения.'; String get chat_noMessages => 'Няма съобщения.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Изпратете съобщение';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2019,9 +2017,18 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get room_management => 'Управление на сървъра за стая'; String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_guest => 'Информация за ретранслаторите';
@override
String get room_guest => 'Информация за сървъра на стаята';
@override @override
String get repeater_managementTools => 'Инструменти за управление'; String get repeater_managementTools => 'Инструменти за управление';
@override
String get repeater_guestTools => 'Инструменти за гости';
@override @override
String get repeater_status => 'Статус'; String get repeater_status => 'Статус';
@@ -2056,6 +2063,14 @@ class AppLocalizationsBg extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Конфигурирайте параметрите на репитера'; 'Конфигурирайте параметрите на репитера';
@override
String get repeater_clockSyncAfterLogin =>
'Синхронизиране на часовника след влизане';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.';
@override @override
String get repeater_statusTitle => 'Статус на повтарянето'; String get repeater_statusTitle => 'Статус на повтарянето';
+19 -4
View File
@@ -435,9 +435,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Ort in der Anzeige einbeziehen'; 'Ort in der Anzeige einbeziehen';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Mehrere Bestätigungen';
return 'Mehrfach-Bestätigungen: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert'; String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
@@ -1239,7 +1237,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get chat_noMessages => 'Noch keine Nachrichten.'; String get chat_noMessages => 'Noch keine Nachrichten.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Nachricht senden';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2017,9 +2015,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get room_management => 'Raum-Server-Verwaltung'; String get room_management => 'Raum-Server-Verwaltung';
@override
String get repeater_guest => 'Informationen zu Repeatern';
@override
String get room_guest => 'Informationen zum Room Server';
@override @override
String get repeater_managementTools => 'Verwaltungs-Tools'; String get repeater_managementTools => 'Verwaltungs-Tools';
@override
String get repeater_guestTools => 'Gastwerkzeuge';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2052,6 +2059,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren'; String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren';
@override
String get repeater_clockSyncAfterLogin =>
'Uhrzeit-Synchronisation nach dem Anmelden';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.';
@override @override
String get repeater_statusTitle => 'Repeaterstatus'; String get repeater_statusTitle => 'Repeaterstatus';
+19 -5
View File
@@ -427,9 +427,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Include location in advert.'; String get settings_advertLocationSubtitle => 'Include location in advert.';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Multi-ACKs';
return 'Multi-ACKs: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Telemetry mode updated'; String get settings_telemetryModeUpdated => 'Telemetry mode updated';
@@ -1871,11 +1869,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get login_repeaterDescription => String get login_repeaterDescription =>
'Enter the repeater password to access settings and status.'; 'Enter the repeater password for guest or admin access.';
@override @override
String get login_roomDescription => String get login_roomDescription =>
'Enter the room password to access settings and status.'; 'Enter the room password for guest or admin access.';
@override @override
String get login_routing => 'Routing'; String get login_routing => 'Routing';
@@ -1979,9 +1977,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get room_management => 'Room Server Management'; String get room_management => 'Room Server Management';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override @override
String get repeater_managementTools => 'Management Tools'; String get repeater_managementTools => 'Management Tools';
@override
String get repeater_guestTools => 'Guest Tools';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2014,6 +2021,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configure repeater parameters'; String get repeater_settingsSubtitle => 'Configure repeater parameters';
@override
String get repeater_clockSyncAfterLogin => 'Clock sync after login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatically send \"clock sync\" after a successful login';
@override @override
String get repeater_statusTitle => 'Repeater Status'; String get repeater_statusTitle => 'Repeater Status';
+19 -4
View File
@@ -434,9 +434,7 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio'; String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Múltiples respuestas de confirmación';
return 'Multi-ACKs: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado'; String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
@@ -1239,7 +1237,7 @@ class AppLocalizationsEs extends AppLocalizations {
String get chat_noMessages => 'Aún no hay mensajes'; String get chat_noMessages => 'Aún no hay mensajes';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Enviar mensaje';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2015,9 +2013,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get room_management => 'Administración del Servidor de Habitación'; String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_guest => 'Información sobre repetidores';
@override
String get room_guest => 'Información del servidor';
@override @override
String get repeater_managementTools => 'Herramientas de Gestión'; String get repeater_managementTools => 'Herramientas de Gestión';
@override
String get repeater_guestTools => 'Herramientas para invitados';
@override @override
String get repeater_status => 'Estado'; String get repeater_status => 'Estado';
@@ -2050,6 +2057,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor'; String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronización del reloj después de iniciar sesión';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.';
@override @override
String get repeater_statusTitle => 'Estado del Repetidor'; String get repeater_statusTitle => 'Estado del Repetidor';
+19 -4
View File
@@ -438,9 +438,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Inclure l\'emplacement dans l\'annonce'; 'Inclure l\'emplacement dans l\'annonce';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Plusieurs accusés de réception';
return 'Multi-ACKs : $value';
}
@override @override
String get settings_telemetryModeUpdated => String get settings_telemetryModeUpdated =>
@@ -1244,7 +1242,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get chat_noMessages => 'Aucun message pour le moment.'; String get chat_noMessages => 'Aucun message pour le moment.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Envoyer un message';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2026,9 +2024,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get room_management => 'Administrattion Room Server'; String get room_management => 'Administrattion Room Server';
@override
String get repeater_guest => 'Informations sur les répéteurs';
@override
String get room_guest => 'Informations sur le serveur';
@override @override
String get repeater_managementTools => 'Outils de Gestion'; String get repeater_managementTools => 'Outils de Gestion';
@override
String get repeater_guestTools => 'Outils pour les invités';
@override @override
String get repeater_status => 'État'; String get repeater_status => 'État';
@@ -2062,6 +2069,14 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Configurer les paramètres du répéteur'; 'Configurer les paramètres du répéteur';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronisation de l\'horloge après la connexion';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Envoyer automatiquement une notification \"synchronisation de l\'heure\" après une connexion réussie.';
@override @override
String get repeater_statusTitle => 'État du répéteur'; String get repeater_statusTitle => 'État du répéteur';
+19 -4
View File
@@ -437,9 +437,7 @@ class AppLocalizationsHu extends AppLocalizations {
'A hirdetés tartalmazza a helyszínt.'; 'A hirdetés tartalmazza a helyszínt.';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Többszörös visszaigazolások';
return 'Többszöri visszaigazolások: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'A telemetriamód frissítve'; String get settings_telemetryModeUpdated => 'A telemetriamód frissítve';
@@ -1247,7 +1245,7 @@ class AppLocalizationsHu extends AppLocalizations {
String get chat_noMessages => 'Még nincs üzenet.'; String get chat_noMessages => 'Még nincs üzenet.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Üzenet küldése';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2030,9 +2028,18 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get room_management => 'Szoba-szerver kezelés'; String get room_management => 'Szoba-szerver kezelés';
@override
String get repeater_guest => 'Adatok a repeaterről';
@override
String get room_guest => 'Szoba szerver információk';
@override @override
String get repeater_managementTools => 'Menedzsmentes eszközök'; String get repeater_managementTools => 'Menedzsmentes eszközök';
@override
String get repeater_guestTools => 'Vendégek számára elérhető eszközök';
@override @override
String get repeater_status => 'Állapot'; String get repeater_status => 'Állapot';
@@ -2066,6 +2073,14 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket'; String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket';
@override
String get repeater_clockSyncAfterLogin =>
'Óra szinkronizálás bejelentkezés után';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.';
@override @override
String get repeater_statusTitle => 'Adatkapcsolódás állapot'; String get repeater_statusTitle => 'Adatkapcsolódás állapot';
+19 -4
View File
@@ -437,9 +437,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Includi la posizione nell\'annuncio'; 'Includi la posizione nell\'annuncio';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'ACK multipli';
return 'Multi-ACKs: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata'; String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
@@ -1240,7 +1238,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get chat_noMessages => 'Nessun messaggio ancora'; String get chat_noMessages => 'Nessun messaggio ancora';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Invia messaggio';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2016,9 +2014,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get room_management => 'Gestione del Server di Camera'; String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_guest => 'Informazioni sul ripetitore';
@override
String get room_guest => 'Informazioni sul server';
@override @override
String get repeater_managementTools => 'Strumenti di Gestione'; String get repeater_managementTools => 'Strumenti di Gestione';
@override
String get repeater_guestTools => 'Strumenti per gli ospiti';
@override @override
String get repeater_status => 'Stato'; String get repeater_status => 'Stato';
@@ -2053,6 +2060,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Configura i parametri del ripetitore'; 'Configura i parametri del ripetitore';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronizzazione dell\'orologio dopo il login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.';
@override @override
String get repeater_statusTitle => 'Stato del Ripetitore'; String get repeater_statusTitle => 'Stato del Ripetitore';
+18 -4
View File
@@ -414,9 +414,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get settings_advertLocationSubtitle => '広告に場所を記載してください。'; String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => '複数のACK(応答)';
return '複数のACK$value';
}
@override @override
String get settings_telemetryModeUpdated => 'テレメトリモードが更新されました'; String get settings_telemetryModeUpdated => 'テレメトリモードが更新されました';
@@ -1180,7 +1178,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get chat_noMessages => 'まだメッセージは届いていません'; String get chat_noMessages => 'まだメッセージは届いていません';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'メッセージを送信する';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1932,9 +1930,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get room_management => 'ルームサーバーの管理'; String get room_management => 'ルームサーバーの管理';
@override
String get repeater_guest => '繰り返し送信に関する情報';
@override
String get room_guest => 'ルームサーバーに関する情報';
@override @override
String get repeater_managementTools => '管理ツール'; String get repeater_managementTools => '管理ツール';
@override
String get repeater_guestTools => 'ゲスト向けツール';
@override @override
String get repeater_status => 'ステータス'; String get repeater_status => 'ステータス';
@@ -1965,6 +1972,13 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する'; String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する';
@override
String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'ログインが成功した場合、自動的に「時刻同期」を送信する。';
@override @override
String get repeater_statusTitle => '再送ステータス'; String get repeater_statusTitle => '再送ステータス';
+18 -4
View File
@@ -414,9 +414,7 @@ class AppLocalizationsKo extends AppLocalizations {
String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.'; String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => '다중 ACK';
return '다중 ACK: $value';
}
@override @override
String get settings_telemetryModeUpdated => '텔레메트리 모드 업데이트 완료'; String get settings_telemetryModeUpdated => '텔레메트리 모드 업데이트 완료';
@@ -1175,7 +1173,7 @@ class AppLocalizationsKo extends AppLocalizations {
String get chat_noMessages => '아직 메시지가 없습니다.'; String get chat_noMessages => '아직 메시지가 없습니다.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => '메시지를 보내기';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1929,9 +1927,18 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get room_management => '방 서버 관리'; String get room_management => '방 서버 관리';
@override
String get repeater_guest => '반복 장비 정보';
@override
String get room_guest => '서버 정보';
@override @override
String get repeater_managementTools => '관리 도구'; String get repeater_managementTools => '관리 도구';
@override
String get repeater_guestTools => '손님용 도구';
@override @override
String get repeater_status => '상태'; String get repeater_status => '상태';
@@ -1962,6 +1969,13 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => '리피터 파라미터 설정'; String get repeater_settingsSubtitle => '리피터 파라미터 설정';
@override
String get repeater_clockSyncAfterLogin => '로그인 후 시계 동기화';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.';
@override @override
String get repeater_statusTitle => '반복 장치 상태'; String get repeater_statusTitle => '반복 장치 상태';
+19 -4
View File
@@ -432,9 +432,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Locatie opnemen in advertentie'; 'Locatie opnemen in advertentie';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Meerdere bevestigingen';
return 'Multi-ACKs: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt'; String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
@@ -1228,7 +1226,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get chat_noMessages => 'Nog geen berichten.'; String get chat_noMessages => 'Nog geen berichten.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Verzend bericht';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2003,9 +2001,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get room_management => 'Beheer Server Kamer'; String get room_management => 'Beheer Server Kamer';
@override
String get repeater_guest => 'Informatie over herhalingsapparatuur';
@override
String get room_guest => 'Informatie over de server';
@override @override
String get repeater_managementTools => 'Beheerfuncties'; String get repeater_managementTools => 'Beheerfuncties';
@override
String get repeater_guestTools => 'Gastenfuncties';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2038,6 +2045,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configureer repeaterparameters'; String get repeater_settingsSubtitle => 'Configureer repeaterparameters';
@override
String get repeater_clockSyncAfterLogin =>
'Na het inloggen, klok synchroniseren';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.';
@override @override
String get repeater_statusTitle => 'Status repeater'; String get repeater_statusTitle => 'Status repeater';
+19 -4
View File
@@ -439,9 +439,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Uwzględnij lokalizację w ogłoszeniu'; 'Uwzględnij lokalizację w ogłoszeniu';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
return 'Wielokrotne ACK: $value';
}
@override @override
String get settings_telemetryModeUpdated => String get settings_telemetryModeUpdated =>
@@ -1248,7 +1246,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_noMessages => 'Brak jeszcze wiadomości'; String get chat_noMessages => 'Brak jeszcze wiadomości';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Wyślij wiadomość';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2031,9 +2029,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get room_management => 'Zarządzanie Serwerem Pokoju'; String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_guest => 'Informacje dotyczące urządzenia powtarzającego';
@override
String get room_guest => 'Informacje o serwerze';
@override @override
String get repeater_managementTools => 'Narzędzia Zarządzania'; String get repeater_managementTools => 'Narzędzia Zarządzania';
@override
String get repeater_guestTools => 'Narzędzia dla gości';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2066,6 +2073,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika'; String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronizacja zegara po zalogowaniu';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.';
@override @override
String get repeater_statusTitle => 'Status przekaźnika'; String get repeater_statusTitle => 'Status przekaźnika';
+19 -4
View File
@@ -436,9 +436,7 @@ class AppLocalizationsPt extends AppLocalizations {
'Incluir localização no anúncio'; 'Incluir localização no anúncio';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Multi-ACKs';
return 'Multi-ACKs: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado'; String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
@@ -1239,7 +1237,7 @@ class AppLocalizationsPt extends AppLocalizations {
String get chat_noMessages => 'Ainda não existem mensagens.'; String get chat_noMessages => 'Ainda não existem mensagens.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Enviar mensagem';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2015,9 +2013,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get room_management => 'Gerenciamento de Servidor de Sala'; String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_guest => 'Informações sobre repetidores';
@override
String get room_guest => 'Informações do Servidor';
@override @override
String get repeater_managementTools => 'Ferramentas de Gerenciamento'; String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@override
String get repeater_guestTools => 'Ferramentas para hóspedes';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2050,6 +2057,14 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronização do relógio após o login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.';
@override @override
String get repeater_statusTitle => 'Status do Repetidor'; String get repeater_statusTitle => 'Status do Repetidor';
+19 -4
View File
@@ -436,9 +436,7 @@ class AppLocalizationsRu extends AppLocalizations {
'Включить местоположение в объявление'; 'Включить местоположение в объявление';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Несколько подтверждений';
return 'Мульти-ACK: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен'; String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
@@ -1239,7 +1237,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get chat_noMessages => 'Сообщений пока нет'; String get chat_noMessages => 'Сообщений пока нет';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Отправить сообщение';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2019,9 +2017,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get room_management => 'Управление сервером комнат'; String get room_management => 'Управление сервером комнат';
@override
String get repeater_guest => 'Информация о ретрансляторе';
@override
String get room_guest => 'Информация о сервере';
@override @override
String get repeater_managementTools => 'Инструменты управления'; String get repeater_managementTools => 'Инструменты управления';
@override
String get repeater_guestTools => 'Инструменты для гостей';
@override @override
String get repeater_status => 'Статус'; String get repeater_status => 'Статус';
@@ -2054,6 +2061,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Настройка параметров репитера'; String get repeater_settingsSubtitle => 'Настройка параметров репитера';
@override
String get repeater_clockSyncAfterLogin =>
'Синхронизация часов после входа в систему';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.';
@override @override
String get repeater_statusTitle => 'Статус репитера'; String get repeater_statusTitle => 'Статус репитера';
+19 -4
View File
@@ -430,9 +430,7 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu'; String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Viaceré ACK';
return 'Viaceré ACK: $value';
}
@override @override
String get settings_telemetryModeUpdated => String get settings_telemetryModeUpdated =>
@@ -1227,7 +1225,7 @@ class AppLocalizationsSk extends AppLocalizations {
String get chat_noMessages => 'Zatiaľ žiadne správy.'; String get chat_noMessages => 'Zatiaľ žiadne správy.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Odoslať správu';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2004,9 +2002,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get room_management => 'Správa servera miestnosti'; String get room_management => 'Správa servera miestnosti';
@override
String get repeater_guest => 'Informácie o opakovači';
@override
String get room_guest => 'Informácie o serveri';
@override @override
String get repeater_managementTools => 'Nástroje na správu'; String get repeater_managementTools => 'Nástroje na správu';
@override
String get repeater_guestTools => 'Nástroje pre hostí';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2039,6 +2046,14 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronizácia hodiniek po prihlávení';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.';
@override @override
String get repeater_statusTitle => 'Status opakého zboru'; String get repeater_statusTitle => 'Status opakého zboru';
+18 -4
View File
@@ -430,9 +430,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.'; String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Več potrdil';
return 'Večkratni potrditvi: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen'; String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
@@ -1225,7 +1223,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get chat_noMessages => 'Še ni sporočil.'; String get chat_noMessages => 'Še ni sporočil.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Pošlji sporočilo';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2001,9 +1999,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get room_management => 'Upravljanje stremlišča'; String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_guest => 'Informacije o ponovljalniku';
@override
String get room_guest => 'Informacije o strežniku';
@override @override
String get repeater_managementTools => 'Upravne orodje'; String get repeater_managementTools => 'Upravne orodje';
@override
String get repeater_guestTools => 'Naložila za goste';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2038,6 +2045,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Konfigurirajte parametre ponovitelja'; 'Konfigurirajte parametre ponovitelja';
@override
String get repeater_clockSyncAfterLogin => 'Sinhronizacija ure po prijavi';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.';
@override @override
String get repeater_statusTitle => 'Status ponovitelja'; String get repeater_statusTitle => 'Status ponovitelja';
+19 -4
View File
@@ -428,9 +428,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen'; String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Flera bekräftelser';
return 'Multi-ACKs: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat'; String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
@@ -1218,7 +1216,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get chat_noMessages => 'Inga meddelanden ännu'; String get chat_noMessages => 'Inga meddelanden ännu';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Skicka meddelande';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1990,9 +1988,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get room_management => 'Rumserverhantering'; String get room_management => 'Rumserverhantering';
@override
String get repeater_guest => 'Information om repetorer';
@override
String get room_guest => 'Information om servern';
@override @override
String get repeater_managementTools => 'Administrationsverktyg'; String get repeater_managementTools => 'Administrationsverktyg';
@override
String get repeater_guestTools => 'Gästverktyg';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2025,6 +2032,14 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar'; String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar';
@override
String get repeater_clockSyncAfterLogin =>
'Synkronisera klockan efter inloggning';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.';
@override @override
String get repeater_statusTitle => 'Återspelsstatus'; String get repeater_statusTitle => 'Återspelsstatus';
+18 -4
View File
@@ -432,9 +432,7 @@ class AppLocalizationsUk extends AppLocalizations {
'Включити місце розташування в оголошення'; 'Включити місце розташування в оголошення';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => 'Багато підтверджень';
return 'Багатократне підтвердження: $value';
}
@override @override
String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено'; String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
@@ -1231,7 +1229,7 @@ class AppLocalizationsUk extends AppLocalizations {
String get chat_noMessages => 'Поки немає повідомлень.'; String get chat_noMessages => 'Поки немає повідомлень.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Надіслати повідомлення';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2014,9 +2012,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get room_management => 'Адміністрування сервера кімнати'; String get room_management => 'Адміністрування сервера кімнати';
@override
String get repeater_guest => 'Інформація про ретранслятор';
@override
String get room_guest => 'Інформація про сервер кімнати';
@override @override
String get repeater_managementTools => 'Інструменти керування'; String get repeater_managementTools => 'Інструменти керування';
@override
String get repeater_guestTools => 'Інструменти для гостей';
@override @override
String get repeater_status => 'Статус'; String get repeater_status => 'Статус';
@@ -2050,6 +2057,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора'; String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора';
@override
String get repeater_clockSyncAfterLogin => 'Синхронізація годин після входу';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.';
@override @override
String get repeater_statusTitle => 'Статус ретранслятора'; String get repeater_statusTitle => 'Статус ретранслятора';
+17 -4
View File
@@ -408,9 +408,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get settings_advertLocationSubtitle => '在广告中包含位置'; String get settings_advertLocationSubtitle => '在广告中包含位置';
@override @override
String settings_multiAck(String value) { String get settings_multiAck => '多重ACK';
return '多重ACK$value';
}
@override @override
String get settings_telemetryModeUpdated => '遥测模式已更新'; String get settings_telemetryModeUpdated => '遥测模式已更新';
@@ -1162,7 +1160,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get chat_noMessages => '暂无消息'; String get chat_noMessages => '暂无消息';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => '发送消息';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1890,9 +1888,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get room_management => '房间服务器管理'; String get room_management => '房间服务器管理';
@override
String get repeater_guest => '重复器信息';
@override
String get room_guest => '服务器信息';
@override @override
String get repeater_managementTools => '管理工具'; String get repeater_managementTools => '管理工具';
@override
String get repeater_guestTools => '访客工具';
@override @override
String get repeater_status => '状态'; String get repeater_status => '状态';
@@ -1923,6 +1930,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => '配置转发节点参数'; String get repeater_settingsSubtitle => '配置转发节点参数';
@override
String get repeater_clockSyncAfterLogin => '登录后,自动同步时钟';
@override
String get repeater_clockSyncAfterLoginSubtitle => '在成功登录后,自动发送“时钟同步”指令。';
@override @override
String get repeater_statusTitle => '转发节点状态'; String get repeater_statusTitle => '转发节点状态';
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Laatst gezien", "contact_lastSeen": "Laatst gezien",
"contact_clearChat": "Chat leegmaken", "contact_clearChat": "Chat leegmaken",
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe", "contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.", "appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.",
"appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route", "appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route",
"appSettings_maxRouteWeight": "Maximale gewicht voor de route", "appSettings_maxRouteWeight": "Maximale gewicht voor de route",
@@ -1941,7 +1934,6 @@
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd", "appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt", "settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Herhalingssleutel overlapt", "map_showOverlaps": "Herhalingssleutel overlapt",
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad.", "map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad.",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2061,5 +2053,18 @@
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).", "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
"scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN", "scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN",
"repeater_cliQuickDiscovery": "Ontdek Buren", "repeater_cliQuickDiscovery": "Ontdek Buren",
"repeater_cliQuickClockSync": "Kloksynchronisatie" "repeater_cliQuickClockSync": "Kloksynchronisatie",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.",
"repeater_clockSyncAfterLogin": "Na het inloggen, klok synchroniseren",
"repeater_guestTools": "Gastenfuncties",
"room_guest": "Informatie over de server",
"chat_sendMessage": "Verzend bericht",
"repeater_guest": "Informatie over herhalingsapparatuur",
"settings_multiAck": "Meerdere bevestigingen"
} }
+14 -9
View File
@@ -1960,13 +1960,6 @@
"contact_settings": "Ustawienia kontaktowe", "contact_settings": "Ustawienia kontaktowe",
"contact_lastSeen": "Ostatnio widziany", "contact_lastSeen": "Ostatnio widziany",
"contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych", "contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Początkowa waga trasy", "appSettings_initialRouteWeight": "Początkowa waga trasy",
"appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu", "appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu",
"appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek", "appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek",
@@ -1979,7 +1972,6 @@
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej", "appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany", "settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
"settings_multiAck": "Wielokrotne ACK: {value}",
"map_showOverlaps": "Nakładające się klucze przekaźników", "map_showOverlaps": "Nakładające się klucze przekaźników",
"map_runTraceWithReturnPath": "Wróć tą samą ścieżką", "map_runTraceWithReturnPath": "Wróć tą samą ścieżką",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2099,5 +2091,18 @@
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).", "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).",
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth", "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth",
"repeater_cliQuickClockSync": "Synchronizacja zegara", "repeater_cliQuickClockSync": "Synchronizacja zegara",
"repeater_cliQuickDiscovery": "Odkryj Sąsiadów" "repeater_cliQuickDiscovery": "Odkryj Sąsiadów",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Synchronizacja zegara po zalogowaniu",
"repeater_clockSyncAfterLoginSubtitle": "Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.",
"chat_sendMessage": "Wyślij wiadomość",
"repeater_guestTools": "Narzędzia dla gości",
"repeater_guest": "Informacje dotyczące urządzenia powtarzającego",
"room_guest": "Informacje o serwerze",
"settings_multiAck": "Wielokrotne potwierdzenia odbioru"
} }
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_telemetry": "Telemetria", "contact_telemetry": "Telemetria",
"contact_settings": "Configurações de Contato", "contact_settings": "Configurações de Contato",
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica", "contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Peso Inicial da Rota", "appSettings_initialRouteWeight": "Peso Inicial da Rota",
"appSettings_maxRouteWeight": "Peso Máximo da Rota", "appSettings_maxRouteWeight": "Peso Máximo da Rota",
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.", "appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
@@ -1941,7 +1934,7 @@
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.", "appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetria atualizado", "settings_telemetryModeUpdated": "Modo de telemetria atualizado",
"settings_multiAck": "Multi-ACKs: {value}", "settings_multiAck": "Multi-ACKs",
"map_showOverlaps": "Sobreposições da Chave Repeater", "map_showOverlaps": "Sobreposições da Chave Repeater",
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.", "map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2061,5 +2054,17 @@
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).", "scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth", "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
"repeater_cliQuickClockSync": "Sincronização do Relógio", "repeater_cliQuickClockSync": "Sincronização do Relógio",
"repeater_cliQuickDiscovery": "Descobrir Vizinhos" "repeater_cliQuickDiscovery": "Descobrir Vizinhos",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.",
"repeater_clockSyncAfterLogin": "Sincronização do relógio após o login",
"room_guest": "Informações do Servidor",
"chat_sendMessage": "Enviar mensagem",
"repeater_guest": "Informações sobre repetidores",
"repeater_guestTools": "Ferramentas para hóspedes"
} }
+14 -9
View File
@@ -1162,13 +1162,6 @@
"contact_clearChat": "Очистить чат", "contact_clearChat": "Очистить чат",
"contact_lastSeen": "Последний раз видели", "contact_lastSeen": "Последний раз видели",
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией", "contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута", "appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.", "appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов", "appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
@@ -1181,7 +1174,6 @@
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.", "appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрии обновлен", "settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"settings_multiAck": "Мульти-ACK: {value}",
"map_showOverlaps": "Перекрытия ключа повтора", "map_showOverlaps": "Перекрытия ключа повтора",
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути", "map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -1301,5 +1293,18 @@
"scanner_linuxPairingHidePin": "Скрыть PIN", "scanner_linuxPairingHidePin": "Скрыть PIN",
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth", "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
"repeater_cliQuickDiscovery": "Обнаружить Соседей", "repeater_cliQuickDiscovery": "Обнаружить Соседей",
"repeater_cliQuickClockSync": "Синхронизация часов" "repeater_cliQuickClockSync": "Синхронизация часов",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему",
"repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.",
"chat_sendMessage": "Отправить сообщение",
"repeater_guest": "Информация о ретрансляторе",
"room_guest": "Информация о сервере",
"repeater_guestTools": "Инструменты для гостей",
"settings_multiAck": "Несколько подтверждений"
} }
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Naposledy videný", "contact_lastSeen": "Naposledy videný",
"contact_teleBase": "Báza telemetrie", "contact_teleBase": "Báza telemetrie",
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia", "contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.", "appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
"appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty", "appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
"appSettings_initialRouteWeight": "Počiatočná váha trasy", "appSettings_initialRouteWeight": "Počiatočná váha trasy",
@@ -1941,7 +1934,7 @@
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej", "appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný", "settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
"settings_multiAck": "Viaceré ACK: {value}", "settings_multiAck": "Viaceré ACK",
"map_showOverlaps": "Prekrývanie opakovača kľúča", "map_showOverlaps": "Prekrývanie opakovača kľúča",
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.", "map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2061,5 +2054,17 @@
"translation_translationOptions": "Možnosti prekladania", "translation_translationOptions": "Možnosti prekladania",
"translation_systemLanguage": "Jazyk systému", "translation_systemLanguage": "Jazyk systému",
"repeater_cliQuickClockSync": "Synchronizácia hodin", "repeater_cliQuickClockSync": "Synchronizácia hodin",
"repeater_cliQuickDiscovery": "Objaviť susedov" "repeater_cliQuickDiscovery": "Objaviť susedov",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Synchronizácia hodiniek po prihlávení",
"repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.",
"chat_sendMessage": "Odoslať správu",
"repeater_guest": "Informácie o opakovači",
"room_guest": "Informácie o serveri",
"repeater_guestTools": "Nástroje pre hostí"
} }
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_teleEnv": "Okolje telemetrije", "contact_teleEnv": "Okolje telemetrije",
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev", "contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji", "contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.", "appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
"appSettings_initialRouteWeight": "Izvirna teža poti", "appSettings_initialRouteWeight": "Izvirna teža poti",
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti", "appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
@@ -1940,7 +1933,6 @@
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil", "appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno", "appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Večkratni potrditvi: {value}",
"settings_telemetryModeUpdated": "Način telemetrije posodobljen", "settings_telemetryModeUpdated": "Način telemetrije posodobljen",
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja", "map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.", "map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.",
@@ -2061,5 +2053,18 @@
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).", "scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje", "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
"repeater_cliQuickDiscovery": "Odkrijte sosede", "repeater_cliQuickDiscovery": "Odkrijte sosede",
"repeater_cliQuickClockSync": "Usklajevanje ure" "repeater_cliQuickClockSync": "Usklajevanje ure",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.",
"repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi",
"repeater_guest": "Informacije o ponovljalniku",
"chat_sendMessage": "Pošlji sporočilo",
"room_guest": "Informacije o strežniku",
"repeater_guestTools": "Naložila za goste",
"settings_multiAck": "Več potrdil"
} }
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri", "contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
"contact_teleLoc": "Telemetridata plats", "contact_teleLoc": "Telemetridata plats",
"contact_teleLocSubtitle": "Tillåt delning av platsdata", "contact_teleLocSubtitle": "Tillåt delning av platsdata",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar", "appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
"appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten", "appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.", "appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
@@ -1941,7 +1934,6 @@
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.", "appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat", "settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Repeater-nyckelöverlappningar", "map_showOverlaps": "Repeater-nyckelöverlappningar",
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg", "map_runTraceWithReturnPath": "Gå tillbaka på samma väg",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2061,5 +2053,18 @@
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).", "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
"scanner_linuxPairingHidePin": "Dölj PIN", "scanner_linuxPairingHidePin": "Dölj PIN",
"repeater_cliQuickDiscovery": "Upptäck grannar", "repeater_cliQuickDiscovery": "Upptäck grannar",
"repeater_cliQuickClockSync": "Synkronisera klocka" "repeater_cliQuickClockSync": "Synkronisera klocka",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.",
"repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning",
"repeater_guest": "Information om repetorer",
"chat_sendMessage": "Skicka meddelande",
"repeater_guestTools": "Gästverktyg",
"room_guest": "Information om servern",
"settings_multiAck": "Flera bekräftelser"
} }
+14 -9
View File
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Останній раз бачили", "contact_lastSeen": "Останній раз бачили",
"contact_teleEnv": "Середовище телеметрії", "contact_teleEnv": "Середовище телеметрії",
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища", "contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Початкова вартість маршруту", "appSettings_initialRouteWeight": "Початкова вартість маршруту",
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів", "appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
"appSettings_maxRouteWeight": "Максимальна вага маршруту", "appSettings_maxRouteWeight": "Максимальна вага маршруту",
@@ -1941,7 +1934,6 @@
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале", "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрії оновлено", "settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"settings_multiAck": "Багатократне підтвердження: {value}",
"map_showOverlaps": "Перекриття ключа повторювача", "map_showOverlaps": "Перекриття ключа повторювача",
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом", "map_runTraceWithReturnPath": "Повернутися назад тим же шляхом",
"@radioStats_noiseFloor": { "@radioStats_noiseFloor": {
@@ -2061,5 +2053,18 @@
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN", "scanner_linuxPairingHidePin": "Приховати PIN",
"repeater_cliQuickClockSync": "Синхронізація годинника", "repeater_cliQuickClockSync": "Синхронізація годинника",
"repeater_cliQuickDiscovery": "Відкрити сусідів" "repeater_cliQuickDiscovery": "Відкрити сусідів",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
"repeater_guestTools": "Інструменти для гостей",
"repeater_guest": "Інформація про ретранслятор",
"room_guest": "Інформація про сервер кімнати",
"chat_sendMessage": "Надіслати повідомлення",
"settings_multiAck": "Багато підтверджень"
} }
+14 -9
View File
@@ -1927,13 +1927,6 @@
"contact_settings": "联系人设置", "contact_settings": "联系人设置",
"contact_teleLocSubtitle": "允许共享位置数据", "contact_teleLocSubtitle": "允许共享位置数据",
"contact_telemetry": "遥测数据", "contact_telemetry": "遥测数据",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeight": "最大路径重量", "appSettings_maxRouteWeight": "最大路径重量",
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量", "appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
"appSettings_initialRouteWeight": "初始路线权重", "appSettings_initialRouteWeight": "初始路线权重",
@@ -1945,7 +1938,7 @@
"appSettings_maxMessageRetries": "最大消息重试次数", "appSettings_maxMessageRetries": "最大消息重试次数",
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数", "appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"settings_multiAck": "多重ACK{value}", "settings_multiAck": "多重ACK",
"settings_telemetryModeUpdated": "遥测模式已更新", "settings_telemetryModeUpdated": "遥测模式已更新",
"map_showOverlaps": "重复键重叠", "map_showOverlaps": "重复键重叠",
"map_runTraceWithReturnPath": "沿着相同的路径返回", "map_runTraceWithReturnPath": "沿着相同的路径返回",
@@ -2066,5 +2059,17 @@
"translation_translationOptions": "翻译选项", "translation_translationOptions": "翻译选项",
"translation_systemLanguage": "系统语言", "translation_systemLanguage": "系统语言",
"repeater_cliQuickDiscovery": "发现邻居", "repeater_cliQuickDiscovery": "发现邻居",
"repeater_cliQuickClockSync": "同步时钟" "repeater_cliQuickClockSync": "同步时钟",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "登录后,自动同步时钟",
"repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。",
"repeater_guestTools": "访客工具",
"repeater_guest": "重复器信息",
"chat_sendMessage": "发送消息",
"room_guest": "服务器信息"
} }
+14 -89
View File
@@ -1,16 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'l10n/app_localizations.dart'; import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'screens/channel_chat_screen.dart';
import 'screens/chat_screen.dart';
import 'screens/chrome_required_screen.dart'; import 'screens/chrome_required_screen.dart';
import 'screens/discovery_screen.dart';
import 'utils/platform_info.dart'; import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart'; import 'connector/meshcore_connector.dart';
@@ -131,7 +125,7 @@ https://creativecommons.org/licenses/by/4.0/
}); });
} }
class MeshCoreApp extends StatefulWidget { class MeshCoreApp extends StatelessWidget {
final MeshCoreConnector connector; final MeshCoreConnector connector;
final MessageRetryService retryService; final MessageRetryService retryService;
final PathHistoryService pathHistoryService; final PathHistoryService pathHistoryService;
@@ -161,94 +155,26 @@ class MeshCoreApp extends StatefulWidget {
required this.timeoutPredictionService, required this.timeoutPredictionService,
}); });
@override
State<MeshCoreApp> createState() => _MeshCoreAppState();
}
class _MeshCoreAppState extends State<MeshCoreApp> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
StreamSubscription<NotificationTapEvent>? _notificationTapSubscription;
@override
void initState() {
super.initState();
_notificationTapSubscription = NotificationService().onNotificationTapped
.listen(_handleNotificationTap);
}
@override
void dispose() {
_notificationTapSubscription?.cancel();
super.dispose();
}
void _handleNotificationTap(NotificationTapEvent event) {
final navigator = _navigatorKey.currentState;
if (navigator == null) return;
switch (event.type) {
case NotificationTapEventType.message:
if (event.id == null) return;
final contact = widget.connector.findContactByKeyHex(event.id!);
if (contact == null) return;
widget.connector.markContactRead(contact.publicKeyHex);
navigator.push(
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
);
break;
case NotificationTapEventType.channel:
if (event.id == null) return;
final channelIndex = int.tryParse(event.id!);
if (channelIndex == null) return;
final channel = widget.connector.findChannelByIndex(channelIndex);
if (channel == null) return;
widget.connector.markChannelRead(channelIndex);
navigator.push(
MaterialPageRoute(
builder: (_) => ChannelChatScreen(channel: channel),
),
);
break;
case NotificationTapEventType.advert:
// Clear every advert notification the discovery
// list the user is about to see contains them all.
NotificationService().clearAllAdvertNotifications();
final ids = widget.connector.allContacts
.map((c) => c.publicKeyHex)
.toList();
NotificationService().clearAdvertNotifications(ids);
navigator.push(
MaterialPageRoute(builder: (_) => const DiscoveryScreen()),
);
break;
case NotificationTapEventType.batch:
// Batch summaries have no single target; no-op.
break;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider.value(value: widget.connector), ChangeNotifierProvider.value(value: connector),
ChangeNotifierProvider.value(value: widget.retryService), ChangeNotifierProvider.value(value: retryService),
ChangeNotifierProvider.value(value: widget.pathHistoryService), ChangeNotifierProvider.value(value: pathHistoryService),
ChangeNotifierProvider.value(value: widget.appSettingsService), ChangeNotifierProvider.value(value: appSettingsService),
ChangeNotifierProvider.value(value: widget.bleDebugLogService), ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: widget.appDebugLogService), ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: widget.chatTextScaleService), ChangeNotifierProvider.value(value: chatTextScaleService),
ChangeNotifierProvider.value(value: widget.translationService), ChangeNotifierProvider.value(value: translationService),
ChangeNotifierProvider.value(value: widget.uiViewStateService), ChangeNotifierProvider.value(value: uiViewStateService),
Provider.value(value: widget.storage), Provider.value(value: storage),
Provider.value(value: widget.mapTileCacheService), Provider.value(value: mapTileCacheService),
ChangeNotifierProvider.value(value: widget.timeoutPredictionService), ChangeNotifierProvider.value(value: timeoutPredictionService),
], ],
child: Consumer<AppSettingsService>( child: Consumer<AppSettingsService>(
builder: (context, settingsService, child) { builder: (context, settingsService, child) {
return WithForegroundTask( return MaterialApp(
child: MaterialApp(
navigatorKey: _navigatorKey,
title: 'MeshCore Open', title: 'MeshCore Open',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
localizationsDelegates: const [ localizationsDelegates: const [
@@ -290,7 +216,6 @@ class _MeshCoreAppState extends State<MeshCoreApp> {
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen() ? const ChromeRequiredScreen()
: const ScannerScreen(), : const ScannerScreen(),
),
); );
}, },
), ),
+4 -2
View File
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../services/app_debug_log_service.dart'; import '../services/app_debug_log_service.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
class AppDebugLogScreen extends StatelessWidget { class AppDebugLogScreen extends StatelessWidget {
const AppDebugLogScreen({super.key}); const AppDebugLogScreen({super.key});
@@ -34,8 +35,9 @@ class AppDebugLogScreen extends StatelessWidget {
.join('\n'); .join('\n');
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.debugLog_copied)), context,
content: Text(context.l10n.debugLog_copied),
); );
} }
: null, : null,
+41 -43
View File
@@ -10,6 +10,7 @@ import '../services/app_settings_service.dart';
import '../services/notification_service.dart'; import '../services/notification_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'map_cache_screen.dart'; import 'map_cache_screen.dart';
class AppSettingsScreen extends StatelessWidget { class AppSettingsScreen extends StatelessWidget {
@@ -151,13 +152,12 @@ class AppSettingsScreen extends StatelessWidget {
.requestPermissions(); .requestPermissions();
if (!granted) { if (!granted) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.appSettings_notificationPermissionDenied, context.l10n.appSettings_notificationPermissionDenied,
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
} }
return; return;
@@ -166,15 +166,14 @@ class AppSettingsScreen extends StatelessWidget {
await settingsService.setNotificationsEnabled(value); await settingsService.setNotificationsEnabled(value);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_notificationsEnabled ? context.l10n.appSettings_notificationsEnabled
: context.l10n.appSettings_notificationsDisabled, : context.l10n.appSettings_notificationsDisabled,
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
} }
}, },
@@ -301,15 +300,14 @@ class AppSettingsScreen extends StatelessWidget {
value: settingsService.settings.clearPathOnMaxRetry, value: settingsService.settings.clearPathOnMaxRetry,
onChanged: (value) { onChanged: (value) {
settingsService.setClearPathOnMaxRetry(value); settingsService.setClearPathOnMaxRetry(value);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_pathsWillBeCleared ? context.l10n.appSettings_pathsWillBeCleared
: context.l10n.appSettings_pathsWillNotBeCleared, : context.l10n.appSettings_pathsWillNotBeCleared,
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
}, },
), ),
@@ -329,15 +327,14 @@ class AppSettingsScreen extends StatelessWidget {
value: settingsService.settings.autoRouteRotationEnabled, value: settingsService.settings.autoRouteRotationEnabled,
onChanged: (value) { onChanged: (value) {
settingsService.setAutoRouteRotationEnabled(value); settingsService.setAutoRouteRotationEnabled(value);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_autoRouteRotationEnabled ? context.l10n.appSettings_autoRouteRotationEnabled
: context.l10n.appSettings_autoRouteRotationDisabled, : context.l10n.appSettings_autoRouteRotationDisabled,
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
}, },
), ),
@@ -1065,25 +1062,25 @@ class AppSettingsScreen extends StatelessWidget {
children: [ children: [
Text(context.l10n.appSettings_showNodesDiscoveredWithin), Text(context.l10n.appSettings_showNodesDiscoveredWithin),
const SizedBox(height: 16), const SizedBox(height: 16),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_allTime), title: Text(context.l10n.appSettings_allTime),
leading: Radio<double>(value: 0), value: 0,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_lastHour), title: Text(context.l10n.appSettings_lastHour),
leading: Radio<double>(value: 1), value: 1,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_last6Hours), title: Text(context.l10n.appSettings_last6Hours),
leading: Radio<double>(value: 6), value: 6,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_last24Hours), title: Text(context.l10n.appSettings_last24Hours),
leading: Radio<double>(value: 24), value: 24,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_lastWeek), title: Text(context.l10n.appSettings_lastWeek),
leading: Radio<double>(value: 168), value: 168,
), ),
], ],
), ),
@@ -1117,13 +1114,13 @@ class AppSettingsScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( RadioListTile<UnitSystem>(
title: Text(context.l10n.appSettings_unitsMetric), title: Text(context.l10n.appSettings_unitsMetric),
leading: const Radio<UnitSystem>(value: UnitSystem.metric), value: UnitSystem.metric,
), ),
ListTile( RadioListTile<UnitSystem>(
title: Text(context.l10n.appSettings_unitsImperial), title: Text(context.l10n.appSettings_unitsImperial),
leading: const Radio<UnitSystem>(value: UnitSystem.imperial), value: UnitSystem.imperial,
), ),
], ],
), ),
@@ -1164,8 +1161,9 @@ class AppSettingsScreen extends StatelessWidget {
String? id, String? id,
}) async { }) async {
if (sourceUrl.isEmpty) { if (sourceUrl.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.translation_enterUrlFirst)), context,
content: Text(context.l10n.translation_enterUrlFirst),
); );
return; return;
} }
@@ -1176,23 +1174,24 @@ class AppSettingsScreen extends StatelessWidget {
id: id, id: id,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.translation_modelDownloaded)), context,
content: Text(context.l10n.translation_modelDownloaded),
); );
await settingsService.setTranslationEnabled(true); await settingsService.setTranslationEnabled(true);
} on TranslationDownloadCancelled { } on TranslationDownloadCancelled {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.translation_downloadStopped)), context,
content: Text(context.l10n.translation_downloadStopped),
); );
} catch (error) { } catch (error) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.translation_downloadFailed(error.toString()), context.l10n.translation_downloadFailed(error.toString()),
), ),
),
); );
} }
} }
@@ -1236,16 +1235,16 @@ class AppSettingsScreen extends StatelessWidget {
try { try {
await translationService.removeModel(model); await translationService.removeModel(model);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
// TODO: l10n // TODO: l10n
content: Text('Deleted ${translationModelFriendlyName(model)}.'), content: Text('Deleted ${translationModelFriendlyName(model)}.'),
),
); );
} catch (error) { } catch (error) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text('Delete failed: $error')), context,
content: Text('Delete failed: $error'),
); // TODO: l10n ); // TODO: l10n
} }
} }
@@ -1279,15 +1278,14 @@ class AppSettingsScreen extends StatelessWidget {
onChanged: (value) async { onChanged: (value) async {
await settingsService.setAppDebugLogEnabled(value); await settingsService.setAppDebugLogEnabled(value);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_appDebugLoggingEnabled ? context.l10n.appSettings_appDebugLoggingEnabled
: context.l10n.appSettings_appDebugLoggingDisabled, : context.l10n.appSettings_appDebugLoggingDisabled,
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
}, },
), ),
+3 -3
View File
@@ -5,6 +5,7 @@ import '../l10n/l10n.dart';
import '../services/ble_debug_log_service.dart'; import '../services/ble_debug_log_service.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
enum _BleLogView { frames, rawLogRx } enum _BleLogView { frames, rawLogRx }
@@ -52,10 +53,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
.join('\n'); .join('\n');
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.debugLog_bleCopied), content: Text(context.l10n.debugLog_bleCopied),
),
); );
} }
: null, : null,
+38 -24
View File
@@ -13,7 +13,7 @@ import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/gif_helper.dart'; import '../helpers/gif_helper.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../helpers/utf8_length_limiter.dart'; import '../helpers/snack_bar_builder.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
@@ -22,6 +22,7 @@ import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart'; import '../services/chat_text_scale_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
import '../utils/emoji_utils.dart'; import '../utils/emoji_utils.dart';
import '../widgets/byte_count_input.dart';
import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/emoji_picker.dart'; import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart'; import '../widgets/gif_message.dart';
@@ -144,11 +145,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Future<void> _scrollToMessage(String messageId) async { Future<void> _scrollToMessage(String messageId) async {
final key = _messageKeys[messageId]; final key = _messageKeys[messageId];
if (key == null) { if (key == null) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_originalMessageNotFound), content: Text(context.l10n.chat_originalMessageNotFound),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -1093,27 +1093,33 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
); );
} }
return ByteCountedTextField(
return TextField( maxBytes: maxBytes,
controller: _textController, controller: _textController,
focusNode: _textFieldFocusNode, focusNode: _textFieldFocusNode,
inputFormatters: [ hintText: context.l10n.chat_typeMessage,
Utf8LengthLimitingTextInputFormatter(maxBytes), onSubmitted: (_) => _sendMessage(),
], encoder:
textCapitalization: TextCapitalization.sentences, connector.isChannelSmazEnabled(widget.channel.index)
? (text) => connector.prepareChannelOutboundText(
widget.channel.index,
text,
)
: null,
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage, hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
), ),
filled: true,
fillColor: Theme.of(
context,
).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 20,
vertical: 8, vertical: 14,
), ),
), ),
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
); );
}, },
), ),
@@ -1151,9 +1157,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final now = DateTime.now(); final now = DateTime.now();
if (_lastChannelSendAt != null && if (_lastChannelSendAt != null &&
now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) { now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); content: Text(context.l10n.chat_sendCooldown),
);
return; return;
} }
_lastChannelSendAt = now; _lastChannelSendAt = now;
@@ -1194,9 +1201,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
} }
final maxBytes = maxChannelMessageBytes(connector.selfName); final maxBytes = maxChannelMessageBytes(connector.selfName);
if (utf8.encode(messageText).length > maxBytes) { final outboundText = connector.prepareChannelOutboundText(
ScaffoldMessenger.of(context).showSnackBar( widget.channel.index,
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), messageText,
);
if (utf8.encode(outboundText).length > maxBytes) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
); );
return; return;
} }
@@ -1323,17 +1335,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
void _copyMessageText(String text) { void _copyMessageText(String text) {
Clipboard.setData(ClipboardData(text: text)); Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); content: Text(context.l10n.chat_messageCopied),
);
} }
Future<void> _deleteMessage(ChannelMessage message) async { Future<void> _deleteMessage(ChannelMessage message) async {
await context.read<MeshCoreConnector>().deleteChannelMessage(message); await context.read<MeshCoreConnector>().deleteChannelMessage(message);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); content: Text(context.l10n.chat_messageDeleted),
);
} }
String _formatPathPrefixes(Uint8List pathBytes) { String _formatPathPrefixes(Uint8List pathBytes) {
+41 -72
View File
@@ -24,6 +24,7 @@ import '../widgets/empty_state.dart';
import '../widgets/qr_code_display.dart'; import '../widgets/qr_code_display.dart';
import '../widgets/quick_switch_bar.dart'; import '../widgets/quick_switch_bar.dart';
import '../widgets/unread_badge.dart'; import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
import 'channel_chat_screen.dart'; import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart'; import 'community_qr_scanner_screen.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
@@ -809,16 +810,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar(
SnackBar(
content: Text( content: Text(
dialogContext dialogContext
.l10n .l10n
.channels_enterChannelName, .channels_enterChannelName,
), ),
),
); );
return; return;
} }
@@ -837,13 +835,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
nextIndex, nextIndex,
); );
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelAdded( context.l10n.channels_channelAdded(name),
name,
),
),
), ),
); );
} }
@@ -897,16 +892,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final name = nameController.text.trim(); final name = nameController.text.trim();
final pskHex = pskController.text.trim(); final pskHex = pskController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar(
SnackBar(
content: Text( content: Text(
dialogContext dialogContext
.l10n .l10n
.channels_enterChannelName, .channels_enterChannelName,
), ),
),
); );
return; return;
} }
@@ -914,29 +906,23 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try { try {
psk = Channel.parsePskHex(pskHex); psk = Channel.parsePskHex(pskHex);
} on FormatException { } on FormatException {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar(
SnackBar(
content: Text( content: Text(
dialogContext dialogContext
.l10n .l10n
.channels_pskMustBe32Hex, .channels_pskMustBe32Hex,
), ),
),
); );
return; return;
} }
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk); connector.setChannel(nextIndex, name, psk);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelAdded( context.l10n.channels_channelAdded(name),
name,
),
),
), ),
); );
} }
@@ -967,12 +953,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.setChannel(nextIndex, 'Public', psk); connector.setChannel(nextIndex, 'Public', psk);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_publicChannelAdded, context.l10n.channels_publicChannelAdded,
), ),
),
); );
} }
}, },
@@ -1097,16 +1082,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async { onPressed: () async {
var hashtag = hashtagController.text.trim(); var hashtag = hashtagController.text.trim();
if (hashtag.isEmpty) { if (hashtag.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar(
SnackBar(
content: Text( content: Text(
dialogContext dialogContext
.l10n .l10n
.channels_enterChannelName, .channels_enterChannelName,
), ),
),
); );
return; return;
} }
@@ -1125,16 +1107,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
} else { } else {
// Community hashtag - HMAC derivation from community secret // Community hashtag - HMAC derivation from community secret
if (selectedCommunity == null) { if (selectedCommunity == null) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, dialogContext,
).showSnackBar(
SnackBar(
content: Text( content: Text(
dialogContext dialogContext
.l10n .l10n
.community_selectCommunity, .community_selectCommunity,
), ),
),
); );
return; return;
} }
@@ -1159,14 +1138,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
psk, psk,
); );
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelAdded( context.l10n.channels_channelAdded(
channelName, channelName,
), ),
), ),
),
); );
} }
}, },
@@ -1259,14 +1237,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar(
SnackBar(
content: Text( content: Text(
dialogContext.l10n.community_enterName, dialogContext.l10n.community_enterName,
), ),
),
); );
return; return;
} }
@@ -1301,12 +1276,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
_loadCommunities(); _loadCommunities();
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.community_created(name), context.l10n.community_created(name),
), ),
),
); );
// Show QR code dialog // Show QR code dialog
@@ -1494,10 +1468,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try { try {
psk = Channel.parsePskHex(pskHex); psk = Channel.parsePskHex(pskHex);
} on FormatException { } on FormatException {
ScaffoldMessenger.of(dialogContext).showSnackBar( showDismissibleSnackBar(
SnackBar( dialogContext,
content: Text(dialogContext.l10n.channels_pskMustBe32Hex), content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
),
); );
return; return;
} }
@@ -1510,16 +1483,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
smazEnabled, smazEnabled,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.channels_channelUpdated(name)), content: Text(context.l10n.channels_channelUpdated(name)),
),
); );
} catch (e, st) { } catch (e, st) {
debugPrint(st.toString()); debugPrint(st.toString());
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text('Failed to update channel: $e')), context,
content: Text('Failed to update channel: $e'),
); );
} }
}, },
@@ -1559,22 +1532,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelDeleted(channel.name), context.l10n.channels_channelDeleted(channel.name),
), ),
),
); );
} catch (e, st) { } catch (e, st) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelDeleteFailed(channel.name), context.l10n.channels_channelDeleteFailed(channel.name),
), ),
),
); );
// Preserve existing logging (if it was there) // Preserve existing logging (if it was there)
@@ -1594,8 +1565,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) { void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
final psk = Channel.parsePskHex(Channel.publicChannelPsk); final psk = Channel.parsePskHex(Channel.publicChannelPsk);
connector.setChannel(0, 'Public', psk); connector.setChannel(0, 'Public', psk);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.channels_publicChannelAdded)), context,
content: Text(context.l10n.channels_publicChannelAdded),
); );
} }
@@ -1810,12 +1782,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
_loadCommunities(); _loadCommunities();
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(context.l10n.community_deleted(community.name)),
context.l10n.community_deleted(community.name),
),
),
); );
} }
}, },
+55 -41
View File
@@ -18,7 +18,6 @@ import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart'; import '../helpers/chat_scroll_controller.dart';
import '../helpers/gif_helper.dart'; import '../helpers/gif_helper.dart';
import '../helpers/path_helper.dart'; import '../helpers/path_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../models/message.dart'; import '../models/message.dart';
@@ -30,6 +29,7 @@ import '../services/path_history_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/elements_ui.dart'; import '../widgets/elements_ui.dart';
import '../widgets/byte_count_input.dart';
import 'channel_message_path_screen.dart'; import 'channel_message_path_screen.dart';
import 'map_screen.dart'; import 'map_screen.dart';
import '../utils/emoji_utils.dart'; import '../utils/emoji_utils.dart';
@@ -43,6 +43,7 @@ import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart'; import '../widgets/translated_message_content.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../helpers/snack_bar_builder.dart';
import 'telemetry_screen.dart'; import 'telemetry_screen.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
@@ -566,24 +567,35 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
); );
} }
return ByteCountedTextField(
return TextField( maxBytes: maxBytes,
controller: _textController, controller: _textController,
focusNode: _textFieldFocusNode, focusNode: _textFieldFocusNode,
inputFormatters: [ hintText: context.l10n.chat_typeMessage,
Utf8LengthLimitingTextInputFormatter(maxBytes), onSubmitted: (_) => _sendMessage(connector),
], encoder:
textCapitalization: TextCapitalization.sentences, connector.isContactSmazEnabled(
widget.contact.publicKeyHex,
)
? (text) => connector.prepareContactOutboundText(
widget.contact,
text,
)
: null,
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage, hintText: context.l10n.chat_typeMessage,
border: const OutlineInputBorder(), border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
filled: true,
fillColor: Theme.of(
context,
).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 20,
vertical: 12, vertical: 14,
), ),
), ),
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(connector),
); );
}, },
), ),
@@ -633,9 +645,10 @@ class _ChatScreenState extends State<ChatScreen> {
final now = DateTime.now(); final now = DateTime.now();
if (_lastTextSendAt != null && if (_lastTextSendAt != null &&
now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) { now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); content: Text(context.l10n.chat_sendCooldown),
);
return; return;
} }
_lastTextSendAt = now; _lastTextSendAt = now;
@@ -670,9 +683,14 @@ class _ChatScreenState extends State<ChatScreen> {
} }
} }
final maxBytes = maxContactMessageBytes(); final maxBytes = maxContactMessageBytes();
if (utf8.encode(outgoingText).length > maxBytes) { final outboundText = connector.prepareContactOutboundText(
ScaffoldMessenger.of(context).showSnackBar( _resolveContact(connector),
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), outgoingText,
);
if (utf8.encode(outboundText).length > maxBytes) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
); );
return; return;
} }
@@ -860,15 +878,12 @@ class _ChatScreenState extends State<ChatScreen> {
_showFullPathDialog(context, path.pathBytes), _showFullPathDialog(context, path.pathBytes),
onTap: () async { onTap: () async {
if (path.pathBytes.isEmpty) { if (path.pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context context.l10n.chat_pathDetailsNotAvailable,
.l10n
.chat_pathDetailsNotAvailable,
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -952,11 +967,10 @@ class _ChatScreenState extends State<ChatScreen> {
_resolveContact(connector), _resolveContact(connector),
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_pathCleared), content: Text(context.l10n.chat_pathCleared),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
@@ -982,11 +996,10 @@ class _ChatScreenState extends State<ChatScreen> {
pathLen: -1, pathLen: -1,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_floodModeEnabled), content: Text(context.l10n.chat_floodModeEnabled),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
@@ -1020,11 +1033,10 @@ class _ChatScreenState extends State<ChatScreen> {
void _showFullPathDialog(BuildContext context, List<int> pathBytes) { void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
if (pathBytes.isEmpty) { if (pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_pathDetailsNotAvailable), content: Text(context.l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -1137,11 +1149,10 @@ class _ChatScreenState extends State<ChatScreen> {
: (verified : (verified
? context.l10n.chat_pathDeviceConfirmed ? context.l10n.chat_pathDeviceConfirmed
: context.l10n.chat_pathDeviceNotConfirmed); : context.l10n.chat_pathDeviceNotConfirmed);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_pathSetHops(hopCount, status)), content: Text(context.l10n.chat_pathSetHops(hopCount, status)),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
),
); );
} }
@@ -1490,26 +1501,29 @@ class _ChatScreenState extends State<ChatScreen> {
void _copyMessageText(String text) { void _copyMessageText(String text) {
Clipboard.setData(ClipboardData(text: text)); Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); content: Text(context.l10n.chat_messageCopied),
);
} }
Future<void> _deleteMessage(Message message) async { Future<void> _deleteMessage(Message message) async {
await context.read<MeshCoreConnector>().deleteMessage(message); await context.read<MeshCoreConnector>().deleteMessage(message);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); content: Text(context.l10n.chat_messageDeleted),
);
} }
void _retryMessage(Message message) { void _retryMessage(Message message) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Retry using the contact's current path override setting // Retry using the contact's current path override setting
connector.sendMessage(_resolveContact(connector), message.text); connector.sendMessage(_resolveContact(connector), message.text);
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage))); content: Text(context.l10n.chat_retryingMessage),
);
} }
void _showEmojiPicker(Message message, Contact senderContact) { void _showEmojiPicker(Message message, Contact senderContact) {
+7 -9
View File
@@ -8,6 +8,7 @@ import '../models/community.dart';
import '../storage/community_store.dart'; import '../storage/community_store.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/qr_scanner_widget.dart'; import '../widgets/qr_scanner_widget.dart';
import '../helpers/snack_bar_builder.dart';
/// Screen for scanning community QR codes to join communities. /// Screen for scanning community QR codes to join communities.
/// ///
@@ -76,11 +77,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
} }
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.community_invalidQrCode), content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} finally { } finally {
@@ -93,12 +93,11 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
} }
void _showInvalidQrError(BuildContext context) { void _showInvalidQrError(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.community_invalidQrCode), content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
} }
@@ -229,11 +228,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
} }
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.community_joined(community.name)), content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
// Return to previous screen // Return to previous screen
+50 -48
View File
@@ -27,6 +27,7 @@ import '../widgets/quick_switch_bar.dart';
import '../widgets/repeater_login_dialog.dart'; import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart'; import '../widgets/room_login_dialog.dart';
import '../widgets/unread_badge.dart'; import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart'; import 'channels_screen.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
import 'discovery_screen.dart'; import 'discovery_screen.dart';
@@ -150,9 +151,10 @@ class _ContactsScreenState extends State<ContactsScreen>
} }
void _showGroupsUnavailableMessage(BuildContext context) { void _showGroupsUnavailableMessage(BuildContext context) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.common_loading))); content: Text(context.l10n.common_loading),
);
} }
void _setupFrameListener() { void _setupFrameListener() {
@@ -169,10 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
// Validate packet has expected minimum size (98+ bytes per protocol) // Validate packet has expected minimum size (98+ bytes per protocol)
if (advertPacket.length < 98) { if (advertPacket.length < 98) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_invalidAdvertFormat), content: Text(context.l10n.contacts_invalidAdvertFormat),
),
); );
} }
_pendingOperations.remove(ContactOperationType.export); _pendingOperations.remove(ContactOperationType.export);
@@ -187,24 +188,23 @@ class _ContactsScreenState extends State<ContactsScreen>
if (!mounted) return; if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) { if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImported)), context,
content: Text(context.l10n.contacts_contactImported),
); );
} }
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_zeroHopContactAdvertSent), content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
),
); );
} }
if (_pendingOperations.contains(ContactOperationType.export)) { if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_contactAdvertCopied), content: Text(context.l10n.contacts_contactAdvertCopied),
),
); );
} }
@@ -216,25 +216,22 @@ class _ContactsScreenState extends State<ContactsScreen>
if (!mounted) return; if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) { if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_contactImportFailed), content: Text(context.l10n.contacts_contactImportFailed),
),
); );
} }
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
),
); );
} }
if (_pendingOperations.contains(ContactOperationType.export)) { if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_contactAdvertCopyFailed), content: Text(context.l10n.contacts_contactAdvertCopyFailed),
),
); );
} }
@@ -271,8 +268,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final clipboardData = await Clipboard.getData('text/plain'); final clipboardData = await Clipboard.getData('text/plain');
if (clipboardData == null || clipboardData.text == null) { if (clipboardData == null || clipboardData.text == null) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), context,
content: Text(context.l10n.contacts_clipboardEmpty),
); );
} }
return; return;
@@ -280,8 +278,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final text = clipboardData.text!.trim(); final text = clipboardData.text!.trim();
if (!text.startsWith('meshcore://')) { if (!text.startsWith('meshcore://')) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), context,
content: Text(context.l10n.contacts_invalidAdvertFormat),
); );
} }
return; return;
@@ -294,8 +293,9 @@ class _ContactsScreenState extends State<ContactsScreen>
connector.importContact(importContactFrame); connector.importContact(importContactFrame);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), context,
content: Text(context.l10n.contacts_invalidAdvertFormat),
); );
} }
} }
@@ -330,11 +330,10 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
onTap: () => { onTap: () => {
connector.sendSelfAdvert(flood: false), connector.sendSelfAdvert(flood: false),
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.settings_advertisementSent), content: Text(context.l10n.settings_advertisementSent),
), ),
),
}, },
), ),
PopupMenuItem( PopupMenuItem(
@@ -347,11 +346,10 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
onTap: () => { onTap: () => {
connector.sendSelfAdvert(flood: true), connector.sendSelfAdvert(flood: true),
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.settings_advertisementSent), content: Text(context.l10n.settings_advertisementSent),
), ),
),
}, },
), ),
PopupMenuItem( PopupMenuItem(
@@ -963,13 +961,16 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context, context: context,
builder: (context) => RepeaterLoginDialog( builder: (context) => RepeaterLoginDialog(
repeater: repeater, repeater: repeater,
onLogin: (password) { onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login // Navigate to repeater hub screen after successful login
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => RepeaterHubScreen(
RepeaterHubScreen(repeater: repeater, password: password), repeater: repeater,
password: password,
isAdmin: isAdmin,
),
), ),
); );
}, },
@@ -986,14 +987,18 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context, context: context,
builder: (context) => RoomLoginDialog( builder: (context) => RoomLoginDialog(
room: room, room: room,
onLogin: (password) { onLogin: (password, isAdmin) {
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex); context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
destination == RoomLoginDestination.management destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password) ? RepeaterHubScreen(
repeater: room,
password: password,
isAdmin: isAdmin,
)
: ChatScreen(contact: room), : ChatScreen(contact: room),
), ),
); );
@@ -1146,19 +1151,17 @@ class _ContactsScreenState extends State<ContactsScreen>
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_groupNameRequired), content: Text(context.l10n.contacts_groupNameRequired),
),
); );
return; return;
} }
if (name.toLowerCase() == if (name.toLowerCase() ==
contactsAllGroupsValue.toLowerCase()) { contactsAllGroupsValue.toLowerCase()) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_groupNameReserved), content: Text(context.l10n.contacts_groupNameReserved),
),
); );
return; return;
} }
@@ -1167,12 +1170,11 @@ class _ContactsScreenState extends State<ContactsScreen>
return g.name.toLowerCase() == name.toLowerCase(); return g.name.toLowerCase() == name.toLowerCase();
}); });
if (exists) { if (exists) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.contacts_groupAlreadyExists(name), context.l10n.contacts_groupAlreadyExists(name),
), ),
),
); );
return; return;
} }
+4 -17
View File
@@ -8,11 +8,11 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../services/notification_service.dart';
import '../utils/contact_search.dart'; import '../utils/contact_search.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart'; import '../widgets/list_filter_widget.dart';
import '../helpers/snack_bar_builder.dart';
enum DiscoverySortOption { lastSeen, name, type } enum DiscoverySortOption { lastSeen, name, type }
@@ -32,20 +32,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen; DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen;
Timer? _searchDebounce; Timer? _searchDebounce;
@override
void initState() {
super.initState();
_clearAdvertNotifications();
}
void _clearAdvertNotifications() {
final connector = context.read<MeshCoreConnector>();
final ids = connector.allContacts.map((c) => c.publicKeyHex).toList();
final ns = NotificationService();
ns.clearAllAdvertNotifications();
ns.clearAdvertNotifications(ids);
}
@override @override
void dispose() { void dispose() {
_searchController.dispose(); _searchController.dispose();
@@ -249,8 +235,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
final hexString = pubKeyToHex(contact.rawPacket!); final hexString = pubKeyToHex(contact.rawPacket!);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), context,
content: Text(context.l10n.contacts_contactAdvertCopied),
); );
break; break;
case 'delete_contact': case 'delete_contact':
+11 -9
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart'; import '../services/map_tile_cache_service.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
class MapCacheScreen extends StatefulWidget { class MapCacheScreen extends StatefulWidget {
const MapCacheScreen({super.key}); const MapCacheScreen({super.key});
@@ -112,15 +113,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
Future<void> _startDownload() async { Future<void> _startDownload() async {
final bounds = _selectedBounds; final bounds = _selectedBounds;
if (bounds == null) { if (bounds == null) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.mapCache_selectAreaFirst)), context,
content: Text(context.l10n.mapCache_selectAreaFirst),
); );
return; return;
} }
if (_estimatedTiles == 0) { if (_estimatedTiles == 0) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.mapCache_noTilesToDownload)), context,
content: Text(context.l10n.mapCache_noTilesToDownload),
); );
return; return;
} }
@@ -182,9 +185,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
result.failed, result.failed,
) )
: context.l10n.mapCache_cachedTiles(result.downloaded); : context.l10n.mapCache_cachedTiles(result.downloaded);
ScaffoldMessenger.of( showDismissibleSnackBar(context, content: Text(message));
context,
).showSnackBar(SnackBar(content: Text(message)));
} }
Future<void> _clearCache() async { Future<void> _clearCache() async {
@@ -210,8 +211,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
final cacheService = context.read<MapTileCacheService>(); final cacheService = context.read<MapTileCacheService>();
await cacheService.clearCache(); await cacheService.clearCache();
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.mapCache_offlineCacheCleared)), context,
content: Text(context.l10n.mapCache_offlineCacheCleared),
); );
} }
+19 -9
View File
@@ -29,6 +29,7 @@ import 'chat_screen.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import '../widgets/repeater_login_dialog.dart'; import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart'; import '../widgets/room_login_dialog.dart';
import '../helpers/snack_bar_builder.dart';
import 'repeater_hub_screen.dart'; import 'repeater_hub_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import 'line_of_sight_map_screen.dart'; import 'line_of_sight_map_screen.dart';
@@ -1366,13 +1367,16 @@ class _MapScreenState extends State<MapScreen> {
context: context, context: context,
builder: (context) => RepeaterLoginDialog( builder: (context) => RepeaterLoginDialog(
repeater: repeater, repeater: repeater,
onLogin: (password) { onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login // Navigate to repeater hub screen after successful login
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => RepeaterHubScreen(
RepeaterHubScreen(repeater: repeater, password: password), repeater: repeater,
password: password,
isAdmin: isAdmin,
),
), ),
); );
}, },
@@ -1385,7 +1389,8 @@ class _MapScreenState extends State<MapScreen> {
context: context, context: context,
builder: (context) => RoomLoginDialog( builder: (context) => RoomLoginDialog(
room: room, room: room,
onLogin: (password) { // onLogin(password, isAdmin) isAdmin not used for room caht screen
onLogin: (password, _) {
// Navigate to chat screen after successful login // Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex); context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push( Navigator.push(
@@ -1659,7 +1664,10 @@ class _MapScreenState extends State<MapScreen> {
); );
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!mounted) return; if (!mounted) return;
messenger.showSnackBar(SnackBar(content: Text(successMsg))); showDismissibleSnackBar(
messenger.context,
content: Text(successMsg),
);
}, },
), ),
ListTile( ListTile(
@@ -1681,8 +1689,9 @@ class _MapScreenState extends State<MapScreen> {
required String flags, required String flags,
}) async { }) async {
if (!connector.isConnected) { if (!connector.isConnected) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.map_connectToShareMarkers)), context,
content: Text(context.l10n.map_connectToShareMarkers),
); );
return; return;
} }
@@ -2271,8 +2280,9 @@ class _MapScreenState extends State<MapScreen> {
_points.clear(); _points.clear();
_polylines.clear(); _polylines.clear();
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.map_pathTraceCancelled)), context,
content: Text(l10n.map_pathTraceCancelled),
); );
}, },
tooltip: l10n.common_cancel, tooltip: l10n.common_cancel,
+7 -9
View File
@@ -11,6 +11,7 @@ import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart'; import '../widgets/snr_indicator.dart';
import '../helpers/snack_bar_builder.dart';
class NeighborsScreen extends StatefulWidget { class NeighborsScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -163,11 +164,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_neighborCount = neighborCount; _neighborCount = neighborCount;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.neighbors_receivedData), content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
_statusTimeout?.cancel(); _statusTimeout?.cancel();
if (!mounted) return; if (!mounted) return;
@@ -224,11 +224,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_isLoading = false; _isLoading = false;
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.neighbors_requestTimedOut), content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
_recordStatusResult(false); _recordStatusResult(false);
}); });
@@ -239,11 +238,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.neighbors_errorLoading(e.toString())), content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+4 -2
View File
@@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart';
import '../widgets/debug_frame_viewer.dart'; import '../widgets/debug_frame_viewer.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterCliScreen extends StatefulWidget { class RepeaterCliScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -336,8 +337,9 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
if (_commandController.text.trim().isNotEmpty) { if (_commandController.text.trim().isNotEmpty) {
_sendCommand(showDebug: true); _sendCommand(showDebug: true);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.repeater_enterCommandFirst)), context,
content: Text(l10n.repeater_enterCommandFirst),
); );
} }
}, },
+17 -3
View File
@@ -13,11 +13,13 @@ import 'neighbors_screen.dart';
class RepeaterHubScreen extends StatelessWidget { class RepeaterHubScreen extends StatelessWidget {
final Contact repeater; final Contact repeater;
final String password; final String password;
final bool isAdmin;
const RepeaterHubScreen({ const RepeaterHubScreen({
super.key, super.key,
required this.repeater, required this.repeater,
required this.password, required this.password,
required this.isAdmin,
}); });
@override @override
@@ -33,11 +35,18 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (isAdmin)
Text( Text(
repeater.type == advTypeRepeater repeater.type == advTypeRepeater
? l10n.repeater_management ? l10n.repeater_management
: l10n.room_management, : l10n.room_management,
), ),
if (!isAdmin)
Text(
repeater.type == advTypeRepeater
? l10n.repeater_guest
: l10n.room_guest,
),
Text( Text(
repeater.name, repeater.name,
style: const TextStyle( style: const TextStyle(
@@ -113,6 +122,7 @@ class RepeaterHubScreen extends StatelessWidget {
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
if (isAdmin)
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
@@ -170,7 +180,9 @@ class RepeaterHubScreen extends StatelessWidget {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
l10n.repeater_managementTools, isAdmin
? l10n.repeater_managementTools
: l10n.repeater_guestTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -210,8 +222,9 @@ class RepeaterHubScreen extends StatelessWidget {
); );
}, },
), ),
const SizedBox(height: 12), if (isAdmin) const SizedBox(height: 12),
// CLI button // CLI button
if (isAdmin)
_buildManagementCard( _buildManagementCard(
context, context,
icon: Icons.terminal, icon: Icons.terminal,
@@ -248,8 +261,9 @@ class RepeaterHubScreen extends StatelessWidget {
); );
}, },
), ),
const SizedBox(height: 12), if (isAdmin) const SizedBox(height: 12),
// Settings button // Settings button
if (isAdmin)
_buildManagementCard( _buildManagementCard(
context, context,
icon: Icons.settings, icon: Icons.settings,
+45 -19
View File
@@ -8,7 +8,9 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../services/app_debug_log_service.dart'; import '../services/app_debug_log_service.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../services/storage_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterSettingsScreen extends StatefulWidget { class RepeaterSettingsScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -25,6 +27,8 @@ class RepeaterSettingsScreen extends StatefulWidget {
} }
class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> { class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
final StorageService _storage = StorageService();
bool _isLoading = false; bool _isLoading = false;
bool _hasChanges = false; bool _hasChanges = false;
bool _refreshingBasic = false; bool _refreshingBasic = false;
@@ -59,6 +63,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
bool _repeatEnabled = true; bool _repeatEnabled = true;
bool _allowReadOnly = true; bool _allowReadOnly = true;
bool _privacyMode = false; bool _privacyMode = false;
bool _autoClockSyncAfterLogin = false;
// Advertisement settings // Advertisement settings
bool _advertEnable = true; bool _advertEnable = true;
@@ -464,18 +469,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (mounted) { if (mounted) {
if (successCount > 0) { if (successCount > 0) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.repeater_refreshed(label)), content: Text(l10n.repeater_refreshed(label)),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.repeater_errorRefreshing(label)), content: Text(l10n.repeater_errorRefreshing(label)),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
@@ -566,6 +569,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_lonController.text = widget.repeater.longitude?.toString() ?? ''; _lonController.text = widget.repeater.longitude?.toString() ?? '';
} }
}); });
final autoClockSync = await _storage
.getRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
);
if (!mounted) return;
setState(() {
_autoClockSyncAfterLogin = autoClockSync;
});
} }
Future<void> _saveSettings() async { Future<void> _saveSettings() async {
@@ -653,11 +665,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
}); });
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.repeater_settingsSaved), content: Text(context.l10n.repeater_settingsSaved),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
} }
} catch (e) { } catch (e) {
@@ -666,13 +677,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
}); });
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.repeater_errorSavingSettings(e.toString()), context.l10n.repeater_errorSavingSettings(e.toString()),
), ),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
@@ -1139,6 +1149,21 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
onRefresh: _refreshAllowReadOnly, onRefresh: _refreshAllowReadOnly,
refreshTooltip: l10n.repeater_refreshGuestAccess, refreshTooltip: l10n.repeater_refreshGuestAccess,
), ),
SwitchListTile(
title: Text(l10n.repeater_clockSyncAfterLogin),
subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle),
value: _autoClockSyncAfterLogin,
onChanged: (value) async {
setState(() {
_autoClockSyncAfterLogin = value;
});
await _storage.setRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
value,
);
},
contentPadding: EdgeInsets.zero,
),
// Privacy mode - hidden until fully implemented // Privacy mode - hidden until fully implemented
// _buildFeatureToggleRow( // _buildFeatureToggleRow(
// title: l10n.repeater_privacyMode, // title: l10n.repeater_privacyMode,
@@ -1401,9 +1426,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (command == 'erase') { if (command == 'erase') {
if (mounted) { if (mounted) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly))); content: Text(l10n.repeater_eraseSerialOnly),
);
} }
return; return;
} }
@@ -1425,17 +1451,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
await connector.sendFrame(frame); await connector.sendFrame(frame);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.repeater_commandSent(command))), context,
content: Text(l10n.repeater_commandSent(command)),
); );
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.repeater_errorSendingCommand(e.toString())), content: Text(l10n.repeater_errorSendingCommand(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+6 -9
View File
@@ -12,6 +12,7 @@ import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterStatusScreen extends StatefulWidget { class RepeaterStatusScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -309,11 +310,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.repeater_statusRequestTimeout), content: Text(context.l10n.repeater_statusRequestTimeout),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
_recordStatusResult(false); _recordStatusResult(false);
}); });
@@ -323,13 +323,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_isLoading = false; _isLoading = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
context.l10n.repeater_errorLoadingStatus(e.toString()),
),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
_recordStatusResult(false); _recordStatusResult(false);
+3 -14
View File
@@ -7,10 +7,10 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../services/linux_ble_error_classifier.dart'; import '../services/linux_ble_error_classifier.dart';
import '../services/notification_service.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart'; import '../widgets/device_tile.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'tcp_screen.dart'; import 'tcp_screen.dart';
import 'usb_screen.dart'; import 'usb_screen.dart';
@@ -44,10 +44,6 @@ class _ScannerScreenState extends State<ScannerScreen> {
isCurrentRoute && isCurrentRoute &&
!_changedNavigation) { !_changedNavigation) {
_changedNavigation = true; _changedNavigation = true;
// Prompt for notification permission on first
// connect so notifications work out of the box
// on Android 13+.
NotificationService().requestPermissions();
if (mounted) { if (mounted) {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const ContactsScreen()), MaterialPageRoute(builder: (context) => const ContactsScreen()),
@@ -58,12 +54,6 @@ class _ScannerScreenState extends State<ScannerScreen> {
_connector.addListener(_connectionListener); _connector.addListener(_connectionListener);
// If the app was killed (swipe-away) and relaunched, try to reconnect
// to the last known device so the user doesn't have to scan again.
if (_connector.state == MeshCoreConnectionState.disconnected) {
_connector.tryAutoReconnect();
}
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen( _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
(state) { (state) {
if (mounted) { if (mounted) {
@@ -328,11 +318,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
return; return;
} }
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.scanner_connectionFailed(e.toString())), content: Text(context.l10n.scanner_connectionFailed(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+59 -51
View File
@@ -11,6 +11,7 @@ import '../l10n/l10n.dart';
import '../models/radio_settings.dart'; import '../models/radio_settings.dart';
import '../services/app_debug_log_service.dart'; import '../services/app_debug_log_service.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import '../helpers/snack_bar_builder.dart';
import 'app_settings_screen.dart'; import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart'; import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart'; import 'ble_debug_log_screen.dart';
@@ -513,8 +514,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setNodeName(controller.text); await connector.setNodeName(controller.text);
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_nodeNameUpdated)), context,
content: Text(l10n.settings_nodeNameUpdated),
); );
}, },
child: Text(l10n.common_save), child: Text(l10n.common_save),
@@ -628,10 +630,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final interval = int.tryParse(intervalText); final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) { if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.settings_locationIntervalInvalid), content: Text(l10n.settings_locationIntervalInvalid),
),
); );
return; return;
} }
@@ -639,8 +640,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setCustomVar("gps_interval:$interval"); await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)), context,
content: Text(l10n.settings_locationUpdated),
); );
} }
@@ -660,15 +662,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
: currentLon; : currentLon;
if (lat == null || lon == null) { if (lat == null || lon == null) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)), context,
content: Text(l10n.settings_locationBothRequired),
); );
return; return;
} }
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)), context,
content: Text(l10n.settings_locationInvalid),
); );
return; return;
} }
@@ -676,8 +680,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setNodeLocation(lat: lat, lon: lon); await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)), context,
content: Text(l10n.settings_locationUpdated),
); );
}, },
child: Text(l10n.common_save), child: Text(l10n.common_save),
@@ -691,9 +696,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _syncTime(BuildContext context, MeshCoreConnector connector) { void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
connector.syncTime(); connector.syncTime();
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized))); content: Text(l10n.settings_timeSynchronized),
);
} }
void _confirmReboot(BuildContext context, MeshCoreConnector connector) { void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
@@ -758,23 +764,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return; if (!mounted) return;
switch (result) { switch (result) {
case gpxExportSuccess: case gpxExportSuccess:
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); content: Text(l10n.settings_gpxExportSuccess),
);
case gpxExportNoContacts: case gpxExportNoContacts:
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), context,
content: Text(l10n.settings_gpxExportNoContacts),
); );
break; break;
case gpxExportNotAvailable: case gpxExportNotAvailable:
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), context,
content: Text(l10n.settings_gpxExportNotAvailable),
); );
break; break;
case gpxExportFailed: case gpxExportFailed:
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); content: Text(l10n.settings_gpxExportError),
);
break; break;
} }
} }
@@ -1001,6 +1011,15 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
SwitchListTile(
title: Text(l10n.settings_multiAck),
value: multiAcks == 1,
onChanged: (value) {
setDialogState(() => multiAcks = value ? 1 : 0);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
initialValue: telemetryMode, initialValue: telemetryMode,
decoration: InputDecoration( decoration: InputDecoration(
@@ -1042,21 +1061,6 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
} }
}, },
), ),
const SizedBox(height: 16),
Text(
l10n.settings_multiAck(multiAcks.toString()),
style: Theme.of(context).textTheme.bodyMedium,
),
Slider(
value: multiAcks.toDouble(),
min: 0,
max: 2,
divisions: 2,
label: multiAcks.toString(),
onChanged: (value) {
setDialogState(() => multiAcks = value.round());
},
),
], ],
), ),
), ),
@@ -1077,8 +1081,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
); );
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)), context,
content: Text(l10n.settings_telemetryModeUpdated),
); );
}, },
child: Text(l10n.common_save), child: Text(l10n.common_save),
@@ -1410,18 +1415,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final txPower = int.tryParse(_txPowerController.text); final txPower = int.tryParse(_txPowerController.text);
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) { if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid))); content: Text(l10n.settings_frequencyInvalid),
);
return; return;
} }
final maxTxPower = widget.connector.maxTxPower ?? 22; final maxTxPower = widget.connector.maxTxPower ?? 22;
if (txPower == null || txPower < 0 || txPower > maxTxPower) { if (txPower == null || txPower < 0 || txPower > maxTxPower) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
),
); );
return; return;
} }
@@ -1441,8 +1446,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (knownRepeat) { if (knownRepeat) {
const validRepeatFreqsKHz = {433000, 869000, 918000}; const validRepeatFreqsKHz = {433000, 869000, 918000};
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)), context,
content: Text(l10n.settings_clientRepeatFreqWarning),
); );
return; return;
} }
@@ -1472,14 +1478,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (!mounted) return; if (!mounted) return;
_logRadioSettingsState('Radio settings saved successfully'); _logRadioSettingsState('Radio settings saved successfully');
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), context,
content: Text(l10n.settings_radioSettingsUpdated),
); );
} catch (e) { } catch (e) {
_appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings'); _appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings');
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_error(e.toString()))), context,
content: Text(l10n.settings_error(e.toString())),
); );
} }
Navigator.pop(context); Navigator.pop(context);
+5 -2
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'usb_screen.dart'; import 'usb_screen.dart';
@@ -270,8 +271,10 @@ class _TcpScreenState extends State<TcpScreen> {
void _showError(String message) { void _showError(String message) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red), context,
content: Text(message),
backgroundColor: Colors.red,
); );
} }
+7 -9
View File
@@ -14,6 +14,7 @@ import '../utils/app_logger.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart'; import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../helpers/snack_bar_builder.dart';
class TelemetryScreen extends StatefulWidget { class TelemetryScreen extends StatefulWidget {
final Contact contact; final Contact contact;
@@ -86,11 +87,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoading = false; _isLoading = false;
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.telemetry_requestTimeout), content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
_recordTelemetryResult(false); _recordTelemetryResult(false);
}); });
@@ -137,11 +137,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_parsedTelemetry = parsedTelemetry; _parsedTelemetry = parsedTelemetry;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.telemetry_receivedData), content: Text(context.l10n.telemetry_receivedData),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
_statusTimeout?.cancel(); _statusTimeout?.cancel();
if (!mounted) return; if (!mounted) return;
@@ -182,11 +181,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.telemetry_errorLoading(e.toString())), content: Text(context.l10n.telemetry_errorLoading(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+3 -3
View File
@@ -10,6 +10,7 @@ import '../utils/app_logger.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart'; import '../utils/usb_port_labels.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'scanner_screen.dart'; import 'scanner_screen.dart';
import 'tcp_screen.dart'; import 'tcp_screen.dart';
@@ -383,11 +384,10 @@ class _UsbScreenState extends State<UsbScreen> {
void _showError(Object error) { void _showError(Object error) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(_friendlyErrorMessage(error)), content: Text(_friendlyErrorMessage(error)),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
+8 -90
View File
@@ -1,30 +1,11 @@
import 'package:flutter/widgets.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart';
/// Manages a foreground service (Android) and app lifecycle awareness class BackgroundService {
/// (Android + iOS) to keep the BLE connection alive when the app is
/// backgrounded or swiped away from the recents drawer.
class BackgroundService with WidgetsBindingObserver {
bool _initialized = false; bool _initialized = false;
bool _serviceRunning = false;
/// Optional callback invoked when the OS resumes the app after it was
/// paused or detached. The connector hooks this to trigger a reconnect
/// check so the BLE link is restored promptly.
VoidCallback? onResume;
/// Optional callback invoked when the app is about to be suspended.
/// The connector can use this to persist critical state.
VoidCallback? onPause;
Future<void> initialize() async { Future<void> initialize() async {
if (_initialized) return; if (!PlatformInfo.isAndroid || _initialized) return;
// Register for app lifecycle events on all mobile platforms.
WidgetsBinding.instance.addObserver(this);
if (PlatformInfo.isAndroid) {
FlutterForegroundTask.init( FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions( androidNotificationOptions: AndroidNotificationOptions(
channelId: 'meshcore_background', channelId: 'meshcore_background',
@@ -43,78 +24,30 @@ class BackgroundService with WidgetsBindingObserver {
allowWifiLock: false, allowWifiLock: false,
), ),
); );
}
_initialized = true; _initialized = true;
} }
Future<void> start() async { Future<void> start() async {
if (!PlatformInfo.isMobile) return; if (!PlatformInfo.isAndroid) return;
if (!_initialized) { if (!_initialized) {
await initialize(); await initialize();
} }
// Android: start the foreground service so the OS keeps the process alive
// even when the user swipes the app away.
if (PlatformInfo.isAndroid) {
final running = await FlutterForegroundTask.isRunningService; final running = await FlutterForegroundTask.isRunningService;
if (!running) { if (running) return;
await FlutterForegroundTask.startService( await FlutterForegroundTask.startService(
notificationTitle: 'MeshCore running', notificationTitle: 'MeshCore running',
notificationText: 'Keeping BLE connected', notificationText: 'Keeping BLE connected',
callback: startCallback, callback: startCallback,
); );
} }
}
// iOS: the bluetooth-central UIBackgroundMode (Info.plist) combined with
// CoreBluetooth state restoration (handled by flutter_blue_plus) keeps the
// BLE connection alive. No additional service is needed, but we track
// the logical "running" state so callers behave consistently.
_serviceRunning = true;
}
Future<void> stop() async { Future<void> stop() async {
if (!PlatformInfo.isMobile) return; if (!PlatformInfo.isAndroid) return;
if (PlatformInfo.isAndroid) {
final running = await FlutterForegroundTask.isRunningService; final running = await FlutterForegroundTask.isRunningService;
if (running) { if (!running) return;
await FlutterForegroundTask.stopService(); await FlutterForegroundTask.stopService();
} }
} }
_serviceRunning = false;
}
bool get isRunning => _serviceRunning;
// ---------------------------------------------------------------------------
// WidgetsBindingObserver app lifecycle
// ---------------------------------------------------------------------------
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
onResume?.call();
break;
case AppLifecycleState.paused:
case AppLifecycleState.detached:
onPause?.call();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
break;
}
}
void dispose() {
WidgetsBinding.instance.removeObserver(this);
}
}
// ---------------------------------------------------------------------------
// Foreground-service isolate entry point (Android)
// ---------------------------------------------------------------------------
@pragma('vm:entry-point') @pragma('vm:entry-point')
void startCallback() { void startCallback() {
@@ -123,25 +56,10 @@ void startCallback() {
class _MeshCoreTaskHandler extends TaskHandler { class _MeshCoreTaskHandler extends TaskHandler {
@override @override
Future<void> onStart(DateTime timestamp, TaskStarter starter) async { Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
// The handler runs in a separate isolate. Its purpose is to keep the
// foreground-service notification alive so that Android does not kill
// the main isolate (where the BLE connection lives).
//
// Heavy BLE work stays in the main isolate; we just need the service
// to exist.
}
@override @override
void onRepeatEvent(DateTime timestamp) { void onRepeatEvent(DateTime timestamp) {}
// Periodically update the notification so the system considers the
// service active. This also acts as a heartbeat.
FlutterForegroundTask.updateService(
notificationTitle: 'MeshCore running',
notificationText:
'Connected · ${timestamp.toLocal().hour.toString().padLeft(2, '0')}:${timestamp.toLocal().minute.toString().padLeft(2, '0')}',
);
}
@override @override
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {} Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
+38 -186
View File
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io' show Platform, File; import 'dart:io' show Platform, File;
import 'dart:ui'; import 'dart:ui';
@@ -9,21 +8,6 @@ import '../helpers/reaction_helper.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
enum NotificationTapEventType { message, channel, advert, batch }
/// Payload emitted when the user taps a notification.
class NotificationTapEvent {
// The type of notification tap event [NotificationTapEventType]
final NotificationTapEventType type;
/// For messages: the contact public key hex.
/// For channels: the channel index as a string.
/// For adverts: the contact public key hex.
final String? id;
const NotificationTapEvent({required this.type, this.id});
}
class NotificationService { class NotificationService {
static final NotificationService _instance = NotificationService._internal(); static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance; factory NotificationService() => _instance;
@@ -33,15 +17,6 @@ class NotificationService {
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
bool _isInitialized = false; bool _isInitialized = false;
/// Stream of notification tap events for navigation handling.
final StreamController<NotificationTapEvent> _tapController =
StreamController<NotificationTapEvent>.broadcast();
/// Listen to this stream to handle navigation when a notification
/// is tapped.
Stream<NotificationTapEvent> get onNotificationTapped =>
_tapController.stream;
// Locale for localized notification strings // Locale for localized notification strings
Locale _locale = const Locale('en'); Locale _locale = const Locale('en');
@@ -192,10 +167,6 @@ class NotificationService {
}) async { }) async {
if (!await _ensureInitialized()) return; if (!await _ensureInitialized()) return;
// Group per contact so each conversation is collapsible
// independently in the notification shade.
final groupKey = contactId != null ? 'msg_$contactId' : 'meshcore_messages';
final androidDetails = AndroidNotificationDetails( final androidDetails = AndroidNotificationDetails(
'messages', 'messages',
'Messages', 'Messages',
@@ -204,7 +175,6 @@ class NotificationService {
priority: Priority.high, priority: Priority.high,
icon: '@mipmap/ic_launcher', icon: '@mipmap/ic_launcher',
number: badgeCount, number: badgeCount,
groupKey: groupKey,
); );
final iosDetails = DarwinNotificationDetails( final iosDetails = DarwinNotificationDetails(
@@ -235,13 +205,6 @@ class NotificationService {
notificationDetails: notificationDetails, notificationDetails: notificationDetails,
payload: 'message:$contactId', payload: 'message:$contactId',
); );
await _postGroupSummary(
groupKey: groupKey,
channelId: 'messages',
channelName: 'Messages',
title: contactName,
payload: 'message:$contactId',
);
} catch (e) { } catch (e) {
debugPrint('Failed to show message notification: $e'); debugPrint('Failed to show message notification: $e');
} }
@@ -254,8 +217,6 @@ class NotificationService {
}) async { }) async {
if (!await _ensureInitialized()) return; if (!await _ensureInitialized()) return;
const groupKey = 'meshcore_adverts';
const androidDetails = AndroidNotificationDetails( const androidDetails = AndroidNotificationDetails(
'adverts', 'adverts',
'Advertisements', 'Advertisements',
@@ -263,7 +224,6 @@ class NotificationService {
importance: Importance.defaultImportance, importance: Importance.defaultImportance,
priority: Priority.defaultPriority, priority: Priority.defaultPriority,
icon: '@mipmap/ic_launcher', icon: '@mipmap/ic_launcher',
groupKey: groupKey,
); );
const iosDetails = DarwinNotificationDetails( const iosDetails = DarwinNotificationDetails(
@@ -294,15 +254,6 @@ class NotificationService {
notificationDetails: notificationDetails, notificationDetails: notificationDetails,
payload: 'advert:$contactId', payload: 'advert:$contactId',
); );
await _postGroupSummary(
groupKey: groupKey,
channelId: 'adverts',
channelName: 'Advertisements',
title: _l10n.notification_activityTitle,
payload: 'advert:',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
);
} catch (e) { } catch (e) {
debugPrint('Failed to show advert notification: $e'); debugPrint('Failed to show advert notification: $e');
} }
@@ -316,12 +267,6 @@ class NotificationService {
}) async { }) async {
if (!await _ensureInitialized()) return; if (!await _ensureInitialized()) return;
// Group per channel so each channel is collapsible
// independently in the notification shade.
final groupKey = channelIndex != null
? 'ch_$channelIndex'
: 'meshcore_channels';
final androidDetails = AndroidNotificationDetails( final androidDetails = AndroidNotificationDetails(
'channel_messages', 'channel_messages',
'Channel Messages', 'Channel Messages',
@@ -330,7 +275,6 @@ class NotificationService {
priority: Priority.high, priority: Priority.high,
icon: '@mipmap/ic_launcher', icon: '@mipmap/ic_launcher',
number: badgeCount, number: badgeCount,
groupKey: groupKey,
); );
final iosDetails = DarwinNotificationDetails( final iosDetails = DarwinNotificationDetails(
@@ -366,70 +310,11 @@ class NotificationService {
notificationDetails: notificationDetails, notificationDetails: notificationDetails,
payload: 'channel:$channelIndex', payload: 'channel:$channelIndex',
); );
await _postGroupSummary(
groupKey: groupKey,
channelId: 'channel_messages',
channelName: 'Channel Messages',
title: channelName,
payload: 'channel:$channelIndex',
);
} catch (e) { } catch (e) {
debugPrint('Failed to show channel notification: $e'); debugPrint('Failed to show channel notification: $e');
} }
} }
// ---------------------------------------------------------------
// Android group summary helper
// ---------------------------------------------------------------
// Android requires a notification with setAsGroupSummary for
// each groupKey. This is what the user sees (and taps) when
// the OS collapses individual notifications in a group.
// ---------------------------------------------------------------
/// Post (or replace) the group summary notification for
/// [groupKey]. The summary's [payload] controls where tapping
/// the collapsed group navigates.
Future<void> _postGroupSummary({
required String groupKey,
required String channelId,
required String channelName,
required String title,
required String payload,
Importance importance = Importance.high,
Priority priority = Priority.high,
}) async {
if (!PlatformInfo.isAndroid) return;
final details = AndroidNotificationDetails(
channelId,
channelName,
importance: importance,
priority: priority,
icon: '@mipmap/ic_launcher',
groupKey: groupKey,
setAsGroupSummary: true,
);
// Use a stable ID derived from the groupKey so each
// group's summary replaces itself, never duplicates.
final summaryId = 'summary:$groupKey'.hashCode;
try {
await _notifications.show(
id: summaryId,
title: title,
body: null,
notificationDetails: NotificationDetails(android: details),
payload: payload,
);
} catch (e) {
debugPrint(
'Failed to post group summary '
'($groupKey): $e',
);
}
}
/// Returns a privacy-safe identifier for debug logging. /// Returns a privacy-safe identifier for debug logging.
/// - advert: shows device name (body contains contactName) /// - advert: shows device name (body contains contactName)
/// - message: shows "from: sender" (avoids logging message content) /// - message: shows "from: sender" (avoids logging message content)
@@ -447,42 +332,14 @@ class NotificationService {
void _onNotificationTapped(NotificationResponse response) { void _onNotificationTapped(NotificationResponse response) {
final payload = response.payload; final payload = response.payload;
if (payload == null) return; if (payload != null) {
debugPrint('Notification tapped: $payload'); debugPrint('Notification tapped: $payload');
// Handle navigation based on payload
if (payload.startsWith('message:')) { // This can be extended to navigate to specific screens
final contactId = payload.substring('message:'.length);
_tapController.add(
NotificationTapEvent(
type: NotificationTapEventType.message,
id: contactId,
),
);
} else if (payload.startsWith('channel:')) {
final channelIndex = payload.substring('channel:'.length);
_tapController.add(
NotificationTapEvent(
type: NotificationTapEventType.channel,
id: channelIndex,
),
);
} else if (payload.startsWith('advert:')) {
final contactId = payload.substring('advert:'.length);
_tapController.add(
NotificationTapEvent(
type: NotificationTapEventType.advert,
id: contactId,
),
);
} else if (payload == 'batch') {
_tapController.add(
const NotificationTapEvent(type: NotificationTapEventType.batch),
);
} }
} }
Future<void> cancelAll() async { Future<void> cancelAll() async {
_pendingNotifications.clear();
await _notifications.cancelAll(); await _notifications.cancelAll();
} }
@@ -495,11 +352,6 @@ class NotificationService {
String contactId, String contactId,
int totalUnreadCount, int totalUnreadCount,
) async { ) async {
// Purge any queued notifications for this contact so the batch timer
// doesn't re-post a notification the user has already seen.
_pendingNotifications.removeWhere(
(n) => n.type == _NotificationType.message && n.id == contactId,
);
if (!await _ensureInitialized()) return; if (!await _ensureInitialized()) return;
await _notifications.cancel(id: contactId.hashCode); await _notifications.cancel(id: contactId.hashCode);
await _updateBadge(totalUnreadCount); await _updateBadge(totalUnreadCount);
@@ -510,13 +362,6 @@ class NotificationService {
int channelIndex, int channelIndex,
int totalUnreadCount, int totalUnreadCount,
) async { ) async {
// Purge any queued notifications for this channel so the batch timer
// doesn't re-post a notification the user has already seen.
_pendingNotifications.removeWhere(
(n) =>
n.type == _NotificationType.channelMessage &&
n.id == channelIndex.toString(),
);
if (!await _ensureInitialized()) return; if (!await _ensureInitialized()) return;
await _notifications.cancel(id: channelIndex.hashCode); await _notifications.cancel(id: channelIndex.hashCode);
await _updateBadge(totalUnreadCount); await _updateBadge(totalUnreadCount);
@@ -530,21 +375,6 @@ class NotificationService {
} }
} }
/// Cancel every advert notification including the group
/// summary. Called when the user opens the discovery list
/// (which shows all discovered nodes anyway).
Future<void> clearAllAdvertNotifications() async {
if (!await _ensureInitialized()) return;
// Cancel the group summary.
final summaryId = 'summary:meshcore_adverts'.hashCode;
await _notifications.cancel(id: summaryId);
// Individual adverts are cancelled by the OS when their
// group summary is removed, but on some OEMs we need to
// cancel them explicitly. We don't track IDs, so the
// caller should also pass known IDs through
// clearAdvertNotifications() when available.
}
Future<void> _updateBadge(int count) async { Future<void> _updateBadge(int count) async {
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
// On Apple platforms, set the badge number directly via a silent update. // On Apple platforms, set the badge number directly via a silent update.
@@ -715,13 +545,7 @@ class NotificationService {
Future<void> _showBatchSummary(List<_PendingNotification> batch) async { Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
if (!await _ensureInitialized()) return; if (!await _ensureInitialized()) return;
// Show each notification individually the Android // Group by type
// groupKey on each type will cluster them automatically.
for (final notification in batch) {
await _showNotificationImmediately(notification);
}
// Debug logging
final messages = batch final messages = batch
.where((n) => n.type == _NotificationType.message) .where((n) => n.type == _NotificationType.message)
.toList(); .toList();
@@ -732,20 +556,48 @@ class NotificationService {
.where((n) => n.type == _NotificationType.channelMessage) .where((n) => n.type == _NotificationType.channelMessage)
.toList(); .toList();
// Build summary text using localized plurals
final parts = <String>[]; final parts = <String>[];
if (messages.isNotEmpty) { if (messages.isNotEmpty) {
parts.add('${messages.length} messages'); parts.add(_l10n.notification_messagesCount(messages.length));
} }
if (channelMsgs.isNotEmpty) { if (channelMsgs.isNotEmpty) {
parts.add('${channelMsgs.length} channel msgs'); parts.add(_l10n.notification_channelMessagesCount(channelMsgs.length));
} }
if (adverts.isNotEmpty) { if (adverts.isNotEmpty) {
parts.add('${adverts.length} adverts'); parts.add(_l10n.notification_newNodesCount(adverts.length));
} }
debugPrint(
'[Notification] batch dispatched: ' if (parts.isEmpty) return;
'${parts.join(", ")}',
// Show first few device names in batch summary for debugging (only if adverts exist)
final deviceInfo = adverts.isNotEmpty
? ' (${adverts.take(5).map((n) => n.body).join(', ')}${adverts.length > 5 ? ', ...' : ''})'
: '';
debugPrint('[Notification] batch summary: ${parts.join(", ")}$deviceInfo');
const androidDetails = AndroidNotificationDetails(
'batch_summary',
'Activity Summary',
channelDescription: 'Batched notification summaries',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
icon: '@mipmap/ic_launcher',
); );
const notificationDetails = NotificationDetails(android: androidDetails);
try {
await _notifications.show(
id: 'batch_summary'.hashCode,
title: _l10n.notification_activityTitle,
body: parts.join(', '),
notificationDetails: notificationDetails,
payload: 'batch',
);
} catch (e) {
debugPrint('Failed to show batch summary notification: $e');
}
} }
} }
+34
View File
@@ -7,8 +7,42 @@ class StorageService {
static const String _pathHistoryPrefix = 'path_history_'; static const String _pathHistoryPrefix = 'path_history_';
static const String _pendingMessagesKey = 'pending_messages'; static const String _pendingMessagesKey = 'pending_messages';
static const String _repeaterPasswordsKey = 'repeater_passwords'; static const String _repeaterPasswordsKey = 'repeater_passwords';
static const String _repeaterAutoClockSyncAfterLoginKey =
'repeater_auto_clock_sync_after_login';
static const String _deliveryObservationsKey = 'delivery_observations'; static const String _deliveryObservationsKey = 'delivery_observations';
Future<Map<String, bool>> _loadRepeaterAutoClockSyncAfterLogin() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey);
if (jsonStr == null) return {};
try {
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
return json.map((key, value) => MapEntry(key, value == true));
} catch (e) {
return {};
}
}
Future<bool> getRepeaterAutoClockSyncAfterLoginEnabled(
String repeaterPubKeyHex,
) async {
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
return settings[repeaterPubKeyHex] ?? false;
}
Future<void> setRepeaterAutoClockSyncAfterLoginEnabled(
String repeaterPubKeyHex,
bool enabled,
) async {
final prefs = PrefsManager.instance;
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
settings[repeaterPubKeyHex] = enabled;
final jsonStr = jsonEncode(settings);
await prefs.setString(_repeaterAutoClockSyncAfterLoginKey, jsonStr);
}
Future<void> savePathHistory( Future<void> savePathHistory(
String contactPubKeyHex, String contactPubKeyHex,
ContactPathHistory history, ContactPathHistory history,
-33
View File
@@ -1,33 +0,0 @@
import 'prefs_manager.dart';
class LastDeviceStore {
static const _prefKeyLastDeviceId = 'bg_last_device_id';
static const _prefKeyLastDeviceName = 'bg_last_device_name';
Future<void> persistLastDevice(
String deviceId,
String deviceDisplayName,
) async {
final prefs = PrefsManager.instance;
await prefs.setString(_prefKeyLastDeviceId, deviceId);
await prefs.setString(_prefKeyLastDeviceName, deviceDisplayName);
}
String? getPersistedDeviceId() {
final prefs = PrefsManager.instance;
final deviceId = prefs.getString(_prefKeyLastDeviceId);
return deviceId;
}
String? getPersistedDeviceName() {
final prefs = PrefsManager.instance;
final displayName = prefs.getString(_prefKeyLastDeviceName);
return displayName;
}
Future<void> clearPersistedDevice() async {
final prefs = PrefsManager.instance;
await prefs.remove(_prefKeyLastDeviceId);
await prefs.remove(_prefKeyLastDeviceName);
}
}
+137
View File
@@ -0,0 +1,137 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../helpers/utf8_length_limiter.dart';
/// A [TextField] that displays a live UTF-8 byte counter.
///
/// The counter appears below the field once the user starts typing and changes
/// colour as the limit is approached (orange at 70 %, error-red at 90 %).
///
/// All standard [TextField] behaviour (focus nodes, input actions, decoration
/// overrides, etc.) is forwarded so the widget can be dropped into any screen.
class ByteCountedTextField extends StatelessWidget {
/// Maximum number of UTF-8 bytes allowed.
final int maxBytes;
/// Controller for the text field.
final TextEditingController controller;
/// Optional focus node forwarded to the inner [TextField].
final FocusNode? focusNode;
/// Hint text shown when the field is empty.
final String? hintText;
/// Keyboard action button (defaults to [TextInputAction.send]).
final TextInputAction textInputAction;
/// Called when the user submits via the keyboard action button.
final ValueChanged<String>? onSubmitted;
/// Additional [TextInputFormatter]s applied *before* the byte limiter.
final List<TextInputFormatter> extraFormatters;
/// Text capitalisation forwarded to the inner [TextField].
final TextCapitalization textCapitalization;
/// Optional full [InputDecoration] override. When provided, [hintText] is
/// ignored set it inside the decoration instead.
final InputDecoration? decoration;
/// Ratio (01) at which the counter turns the warning colour (default 0.7).
final double warningThreshold;
/// Ratio (01) at which the counter turns the error colour (default 0.9).
final double errorThreshold;
/// Whether to hide the counter when the field is empty (default `true`).
final bool hideCounterWhenEmpty;
/// Optional encoder function to transform text before byte counting/limiting.
/// If provided, byte limits and counters will use the encoded text length.
final String Function(String)? encoder;
const ByteCountedTextField({
super.key,
required this.maxBytes,
required this.controller,
this.focusNode,
this.hintText,
this.textInputAction = TextInputAction.send,
this.onSubmitted,
this.extraFormatters = const [],
this.textCapitalization = TextCapitalization.sentences,
this.decoration,
this.warningThreshold = 0.7,
this.errorThreshold = 0.9,
this.hideCounterWhenEmpty = true,
this.encoder,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) {
final effectiveText = encoder != null
? encoder!(value.text)
: value.text;
final usedBytes = utf8.encode(effectiveText).length;
final ratio = maxBytes > 0 ? usedBytes / maxBytes : 0.0;
final showCounter = !(hideCounterWhenEmpty && value.text.isEmpty);
final counterColor = ratio > errorThreshold
? Theme.of(context).colorScheme.error
: ratio > warningThreshold
? Colors.orange
: Theme.of(context).colorScheme.onSurfaceVariant;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
maxLines: null,
controller: controller,
focusNode: focusNode,
inputFormatters: [
...extraFormatters,
Utf8LengthLimitingTextInputFormatter(
maxBytes,
encoder: encoder,
),
],
textCapitalization: textCapitalization,
decoration:
decoration ??
InputDecoration(
hintText: hintText,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
textInputAction: textInputAction,
onSubmitted: onSubmitted,
),
if (showCounter)
Padding(
padding: const EdgeInsets.only(top: 4, right: 4),
child: Align(
alignment: Alignment.centerRight,
child: Text(
'$usedBytes / $maxBytes',
style: TextStyle(fontSize: 11, color: counterColor),
),
),
),
],
);
},
);
}
}
+13 -18
View File
@@ -11,6 +11,7 @@ import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../helpers/path_helper.dart'; import '../helpers/path_helper.dart';
import '../services/path_history_service.dart'; import '../services/path_history_service.dart';
import '../helpers/snack_bar_builder.dart';
import 'path_selection_dialog.dart'; import 'path_selection_dialog.dart';
class PathManagementDialog { class PathManagementDialog {
@@ -65,11 +66,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
void _showFullPathDialog(BuildContext context, List<int> pathBytes) { void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
final l10n = context.l10n; final l10n = context.l10n;
if (pathBytes.isEmpty) { if (pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_pathDetailsNotAvailable), content: Text(l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -159,11 +159,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_hopsCount(result.length)), content: Text(l10n.chat_hopsCount(result.length)),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
} }
} }
@@ -337,13 +336,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
_showFullPathDialog(context, path.pathBytes), _showFullPathDialog(context, path.pathBytes),
onTap: () async { onTap: () async {
if (path.pathBytes.isEmpty) { if (path.pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
l10n.chat_pathDetailsNotAvailable, l10n.chat_pathDetailsNotAvailable,
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -361,13 +359,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
if (!context.mounted) return; if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
l10n.path_usingHopsPath(path.hopCount), l10n.path_usingHopsPath(path.hopCount),
), ),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
}, },
), ),
@@ -459,11 +456,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
onTap: () async { onTap: () async {
await connector.clearContactPath(currentContact); await connector.clearContactPath(currentContact);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_pathCleared), content: Text(l10n.chat_pathCleared),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
@@ -489,11 +485,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
pathLen: -1, pathLen: -1,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_floodModeEnabled), content: Text(l10n.chat_floodModeEnabled),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
+6 -9
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../helpers/snack_bar_builder.dart';
class PathSelectionDialog extends StatefulWidget { class PathSelectionDialog extends StatefulWidget {
final List<Contact> availableContacts; final List<Contact> availableContacts;
@@ -138,26 +139,22 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
// Show error for invalid prefixes // Show error for invalid prefixes
if (invalidPrefixes.isNotEmpty) { if (invalidPrefixes.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))),
l10n.path_invalidHexPrefixes(invalidPrefixes.join(", ")),
),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
return; return;
} }
// Check max path length (64 hops) // Check max path length (64 hops)
if (pathBytesList.length > 64) { if (pathBytesList.length > 64) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.path_tooLong), content: Text(l10n.path_tooLong),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
return; return;
} }
+34 -6
View File
@@ -14,7 +14,7 @@ import 'path_management_dialog.dart';
class RepeaterLoginDialog extends StatefulWidget { class RepeaterLoginDialog extends StatefulWidget {
final Contact repeater; final Contact repeater;
final Function(String password) onLogin; final Function(String password, bool isAdmin) onLogin;
const RepeaterLoginDialog({ const RepeaterLoginDialog({
super.key, super.key,
@@ -119,6 +119,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
: '${selection.hopCount} hops'; : '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin'); appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin');
bool? loginResult; bool? loginResult;
bool isAdmin = false;
for (int attempt = 0; attempt < _maxAttempts; attempt++) { for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@@ -131,7 +132,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
); );
await _connector.sendFrame(loginFrame); await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout); (loginResult, isAdmin) = await _awaitLoginResponse(timeout);
if (loginResult == true) { if (loginResult == true) {
appLogger.info( appLogger.info(
'Login succeeded for ${repeater.name}', 'Login succeeded for ${repeater.name}',
@@ -187,9 +188,32 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex); await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex);
} }
final autoClockSync = await _storage
.getRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
);
if (autoClockSync) {
try {
final timestampSeconds =
DateTime.now().millisecondsSinceEpoch ~/ 1000;
await _connector.sendFrame(
buildSendCliCommandFrame(
repeater.publicKey,
'clock sync',
timestampSeconds: timestampSeconds,
),
);
} catch (e) {
appLogger.warn(
'Auto clock sync failed for ${repeater.name}: $e',
tag: 'RepeaterLogin',
);
}
}
if (mounted) { if (mounted) {
Navigator.pop(context, password); Navigator.pop(context, password);
Future.microtask(() => widget.onLogin(password)); Future.microtask(() => widget.onLogin(password, isAdmin));
} }
} catch (e) { } catch (e) {
final repeater = _resolveRepeater(_connector); final repeater = _resolveRepeater(_connector);
@@ -206,17 +230,21 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
} }
} }
Future<bool?> _awaitLoginResponse(Duration timeout) async { // _awaitLoginResponse returns a record of bool, for success and if the client is an admin
Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async {
final completer = Completer<bool?>(); final completer = Completer<bool?>();
Timer? timer; Timer? timer;
StreamSubscription<Uint8List>? subscription; StreamSubscription<Uint8List>? subscription;
final targetPrefix = widget.repeater.publicKey.sublist(0, 6); final targetPrefix = widget.repeater.publicKey.sublist(0, 6);
bool isAdmin = false;
subscription = _connector.receivedFrames.listen((frame) { subscription = _connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return; if (frame.isEmpty) return;
final code = frame[0]; final code = frame[0];
if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return;
if (frame.length < 8) return; if (frame.length < 8) return;
// NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the
// expected client permissions
isAdmin = (frame[1] == 1);
final prefix = frame.sublist(2, 8); final prefix = frame.sublist(2, 8);
if (!listEquals(prefix, targetPrefix)) return; if (!listEquals(prefix, targetPrefix)) return;
@@ -235,7 +263,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
final result = await completer.future; final result = await completer.future;
timer.cancel(); timer.cancel();
await subscription.cancel(); await subscription.cancel();
return result; return (result, isAdmin);
} }
@override @override
+13 -8
View File
@@ -10,11 +10,12 @@ import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../helpers/snack_bar_builder.dart';
import 'path_management_dialog.dart'; import 'path_management_dialog.dart';
class RoomLoginDialog extends StatefulWidget { class RoomLoginDialog extends StatefulWidget {
final Contact room; final Contact room;
final Function(String password) onLogin; final Function(String password, bool isAdmin) onLogin;
const RoomLoginDialog({super.key, required this.room, required this.onLogin}); const RoomLoginDialog({super.key, required this.room, required this.onLogin});
@@ -114,6 +115,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
: '${selection.hopCount} hops'; : '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin'); appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin');
bool? loginResult; bool? loginResult;
bool isAdmin = false;
for (int attempt = 0; attempt < _maxAttempts; attempt++) { for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@@ -126,7 +128,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
); );
await _connector.sendFrame(loginFrame); await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout); (loginResult, isAdmin) = await _awaitLoginResponse(timeout);
if (loginResult == true) { if (loginResult == true) {
appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin'); appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin');
break; break;
@@ -166,7 +168,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
if (mounted) { if (mounted) {
Navigator.pop(context, password); Navigator.pop(context, password);
Future.microtask(() => widget.onLogin(password)); Future.microtask(() => widget.onLogin(password, isAdmin));
} }
} catch (e) { } catch (e) {
final room = _resolveRepeater(_connector); final room = _resolveRepeater(_connector);
@@ -175,26 +177,29 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
setState(() { setState(() {
_isLoggingIn = false; _isLoggingIn = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.login_failed(e.toString())), content: Text(context.l10n.login_failed(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
} }
Future<bool?> _awaitLoginResponse(Duration timeout) async { Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async {
final completer = Completer<bool?>(); final completer = Completer<bool?>();
Timer? timer; Timer? timer;
StreamSubscription<Uint8List>? subscription; StreamSubscription<Uint8List>? subscription;
final targetPrefix = widget.room.publicKey.sublist(0, 6); final targetPrefix = widget.room.publicKey.sublist(0, 6);
bool isAdmin = false;
subscription = _connector.receivedFrames.listen((frame) { subscription = _connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return; if (frame.isEmpty) return;
final code = frame[0]; final code = frame[0];
if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return;
// NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the
// expected client permissions
isAdmin = (frame[1] == 1);
if (frame.length < 8) return; if (frame.length < 8) return;
final prefix = frame.sublist(2, 8); final prefix = frame.sublist(2, 8);
if (!listEquals(prefix, targetPrefix)) return; if (!listEquals(prefix, targetPrefix)) return;
@@ -214,7 +219,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
final result = await completer.future; final result = await completer.future;
timer.cancel(); timer.cancel();
await subscription.cancel(); await subscription.cancel();
return result; return (result, isAdmin);
} }
@override @override
-6
View File
@@ -22,8 +22,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1): - url_launcher_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- flserial (from `Flutter/ephemeral/.symlinks/plugins/flserial/macos`) - flserial (from `Flutter/ephemeral/.symlinks/plugins/flserial/macos`)
@@ -36,7 +34,6 @@ DEPENDENCIES:
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
flserial: flserial:
@@ -59,8 +56,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos: url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
flserial: 3c161e076dfc73458ec5803e7a9a9d2bb85fadf6 flserial: 3c161e076dfc73458ec5803e7a9a9d2bb85fadf6
@@ -73,7 +68,6 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
+176 -360
View File
@@ -1,382 +1,198 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/l10n/app_localizations.dart'; import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/widgets/adaptive_app_bar_title.dart'; import 'package:meshcore_open/screens/scanner_screen.dart';
import 'package:meshcore_open/screens/tcp_screen.dart';
import 'package:meshcore_open/services/app_settings_service.dart';
// --------------------------------------------------------------------------- class _FakeMeshCoreConnector extends MeshCoreConnector {
// Pure helpers extracted from TcpScreen logic so we can unit-test them _FakeMeshCoreConnector();
// without pumping the full screen widget tree.
// ---------------------------------------------------------------------------
/// Mirrors the validation in `_TcpScreenState._connectTcp`. MeshCoreConnectionState initialState = MeshCoreConnectionState.disconnected;
String? validateTcpInputs({required String host, required String portText}) { MeshCoreTransportType initialTransport = MeshCoreTransportType.bluetooth;
if (host.trim().isEmpty) return 'hostRequired'; String? initialEndpoint;
final parsed = int.tryParse(portText.trim()); int connectTcpCalls = 0;
if (parsed == null || parsed < 1 || parsed > 65535) return 'portInvalid'; String? lastHost;
return null; int? lastPort;
@override
MeshCoreConnectionState get state => initialState;
@override
MeshCoreTransportType get activeTransport => initialTransport;
@override
bool get isTcpTransportConnected =>
initialState == MeshCoreConnectionState.connected &&
initialTransport == MeshCoreTransportType.tcp;
@override
String? get activeTcpEndpoint => initialEndpoint;
@override
Future<void> connectTcp({required String host, required int port}) async {
connectTcpCalls += 1;
lastHost = host;
lastPort = port;
}
} }
/// Mirrors `_TcpScreenState._buildStatusBar` text selection. Widget _buildTestApp({
String tcpStatusText({ required MeshCoreConnector connector,
required MeshCoreConnectionState state, required Widget child,
required MeshCoreTransportType transport, Locale? locale,
required bool isTcpConnected,
String? activeTcpEndpoint,
String connectingEndpoint = '',
required String notConnected,
required String Function(String) connectedTo,
required String Function(String) connectingTo,
required String disconnecting,
}) { }) {
if (isTcpConnected) return connectedTo(activeTcpEndpoint ?? 'TCP'); return MultiProvider(
if (state == MeshCoreConnectionState.connecting && providers: [
transport == MeshCoreTransportType.tcp) { ChangeNotifierProvider<MeshCoreConnector>.value(value: connector),
return connectingTo(connectingEndpoint); ChangeNotifierProvider<AppSettingsService>(
} create: (_) => AppSettingsService(),
if (state == MeshCoreConnectionState.disconnecting &&
transport == MeshCoreTransportType.tcp) {
return disconnecting;
}
return notConnected;
}
/// Mirrors `_TcpScreenState._friendlyErrorMessage`.
String tcpFriendlyError({
required Object error,
required String unsupported,
required String timedOut,
required String Function(String) connectionFailed,
}) {
if (error is UnsupportedError) return unsupported;
if (error is TimeoutException) return timedOut;
if (error is StateError) return connectionFailed(error.message);
if (error is ArgumentError) {
return connectionFailed(error.message?.toString() ?? error.toString());
}
return connectionFailed(error.toString());
}
/// Whether the connect button should be disabled.
bool isTcpConnectButtonDisabled({
required MeshCoreConnectionState state,
required MeshCoreTransportType transport,
}) {
final isConnecting =
state == MeshCoreConnectionState.connecting &&
transport == MeshCoreTransportType.tcp;
return isConnecting || state == MeshCoreConnectionState.scanning;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
// -- Validation -----------------------------------------------------------
group('TCP input validation', () {
test('empty host returns hostRequired', () {
expect(validateTcpInputs(host: '', portText: '5000'), 'hostRequired');
});
test('whitespace-only host returns hostRequired', () {
expect(validateTcpInputs(host: ' ', portText: '5000'), 'hostRequired');
});
test('non-numeric port returns portInvalid', () {
expect(
validateTcpInputs(host: '192.168.1.50', portText: 'abc'),
'portInvalid',
);
});
test('port 0 returns portInvalid', () {
expect(
validateTcpInputs(host: '192.168.1.50', portText: '0'),
'portInvalid',
);
});
test('port > 65535 returns portInvalid', () {
expect(
validateTcpInputs(host: '192.168.1.50', portText: '99999'),
'portInvalid',
);
});
test('valid host and port returns null', () {
expect(validateTcpInputs(host: '192.168.1.50', portText: '5000'), isNull);
});
test('port 1 is valid (lower boundary)', () {
expect(validateTcpInputs(host: 'h', portText: '1'), isNull);
});
test('port 65535 is valid (upper boundary)', () {
expect(validateTcpInputs(host: 'h', portText: '65535'), isNull);
});
});
// -- Status text ----------------------------------------------------------
group('TCP status text', () {
String status({
MeshCoreConnectionState state = MeshCoreConnectionState.disconnected,
MeshCoreTransportType transport = MeshCoreTransportType.tcp,
bool isTcpConnected = false,
String? activeTcpEndpoint,
String connectingEndpoint = 'host:5000',
}) => tcpStatusText(
state: state,
transport: transport,
isTcpConnected: isTcpConnected,
activeTcpEndpoint: activeTcpEndpoint,
connectingEndpoint: connectingEndpoint,
notConnected: 'NOT_CONNECTED',
connectedTo: (ep) => 'CONNECTED:$ep',
connectingTo: (ep) => 'CONNECTING:$ep',
disconnecting: 'DISCONNECTING',
);
test('disconnected shows not-connected', () {
expect(status(), 'NOT_CONNECTED');
});
test('connected with endpoint', () {
expect(
status(
state: MeshCoreConnectionState.connected,
isTcpConnected: true,
activeTcpEndpoint: 'server.local:5000',
),
'CONNECTED:server.local:5000',
);
});
test('connected with null endpoint falls back to TCP', () {
expect(
status(state: MeshCoreConnectionState.connected, isTcpConnected: true),
'CONNECTED:TCP',
);
});
test('connecting over TCP shows connecting-to', () {
expect(
status(
state: MeshCoreConnectionState.connecting,
connectingEndpoint: '10.0.0.1:4000',
),
'CONNECTING:10.0.0.1:4000',
);
});
test('disconnecting over TCP shows disconnecting', () {
expect(
status(state: MeshCoreConnectionState.disconnecting),
'DISCONNECTING',
);
});
test('connecting over bluetooth falls through to not-connected', () {
expect(
status(
state: MeshCoreConnectionState.connecting,
transport: MeshCoreTransportType.bluetooth,
),
'NOT_CONNECTED',
);
});
});
// -- Error mapping --------------------------------------------------------
group('TCP friendly error messages', () {
String error(Object e) => tcpFriendlyError(
error: e,
unsupported: 'UNSUPPORTED',
timedOut: 'TIMED_OUT',
connectionFailed: (msg) => 'FAILED:$msg',
);
test('UnsupportedError → unsupported', () {
expect(error(UnsupportedError('nope')), 'UNSUPPORTED');
});
test('TimeoutException → timedOut', () {
expect(error(TimeoutException('slow')), 'TIMED_OUT');
});
test('StateError → connectionFailed with message', () {
expect(error(StateError('refused')), 'FAILED:refused');
});
test('ArgumentError → connectionFailed with message', () {
expect(error(ArgumentError('bad host')), 'FAILED:bad host');
});
test('generic error → connectionFailed with toString', () {
expect(error(Exception('boom')), 'FAILED:Exception: boom');
});
});
// -- Button disabled state ------------------------------------------------
group('TCP connect button disabled state', () {
test('disabled while scanning', () {
expect(
isTcpConnectButtonDisabled(
state: MeshCoreConnectionState.scanning,
transport: MeshCoreTransportType.bluetooth,
),
isTrue,
);
});
test('disabled while connecting over TCP', () {
expect(
isTcpConnectButtonDisabled(
state: MeshCoreConnectionState.connecting,
transport: MeshCoreTransportType.tcp,
),
isTrue,
);
});
test('enabled while connecting over bluetooth (not TCP-specific)', () {
expect(
isTcpConnectButtonDisabled(
state: MeshCoreConnectionState.connecting,
transport: MeshCoreTransportType.bluetooth,
),
isFalse,
);
});
test('enabled when disconnected', () {
expect(
isTcpConnectButtonDisabled(
state: MeshCoreConnectionState.disconnected,
transport: MeshCoreTransportType.tcp,
),
isFalse,
);
});
});
// -- Localized strings resolve correctly ----------------------------------
testWidgets('English TCP localizations resolve without error', (
tester,
) async {
late AppLocalizations l10n;
await tester.pumpWidget(
MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) {
l10n = AppLocalizations.of(context);
return const SizedBox.shrink();
},
),
),
);
await tester.pumpAndSettle();
expect(l10n.tcpScreenTitle, isNotEmpty);
expect(l10n.tcpHostLabel, isNotEmpty);
expect(l10n.tcpPortLabel, isNotEmpty);
expect(l10n.tcpStatus_notConnected, isNotEmpty);
expect(l10n.tcpErrorHostRequired, isNotEmpty);
expect(l10n.tcpErrorPortInvalid, isNotEmpty);
expect(l10n.tcpErrorUnsupported, isNotEmpty);
expect(l10n.tcpErrorTimedOut, isNotEmpty);
expect(l10n.tcpConnectionFailed('x'), contains('x'));
expect(l10n.tcpStatus_connectingTo('host:5000'), contains('host:5000'));
expect(l10n.scanner_connectedTo('device'), contains('device'));
});
// -- Isolated widget: AdaptiveAppBarTitle overflow ------------------------
testWidgets('AdaptiveAppBarTitle does not overflow with long text', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(320, 100));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: SizedBox(
width: 200,
child: AdaptiveAppBarTitle(
'This is a very long title that would normally overflow',
),
),
),
),
);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
expect(
find.text('This is a very long title that would normally overflow'),
findsOneWidget,
);
});
// -- Isolated widget: status bar Row with FittedBox overflow --------------
testWidgets('Status bar row with long text does not overflow at 320px', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(320, 100));
addTearDown(() => tester.binding.setSurfaceSize(null));
const longText =
'Connected to meshcore-room-server-very-long-hostname.local:5000';
const statusColor = Colors.green;
// Exact widget tree from _buildStatusBar in TcpScreen / UsbScreen.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: statusColor.withValues(alpha: 0.1),
child: Row(
children: [
const Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
longText,
style: const TextStyle(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
),
), ),
], ],
child: MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: child,
), ),
);
}
void main() {
testWidgets('TcpScreen uses localized TCP copy', (tester) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
), ),
);
await tester.pumpAndSettle();
final context = tester.element(find.byType(TcpScreen));
final l10n = AppLocalizations.of(context);
expect(find.text(l10n.tcpScreenTitle), findsOneWidget);
expect(find.text(l10n.tcpHostLabel), findsOneWidget);
expect(find.text(l10n.tcpPortLabel), findsOneWidget);
expect(find.text(l10n.tcpStatus_notConnected), findsOneWidget);
});
testWidgets('TcpScreen validation errors are localized', (tester) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
), ),
);
await tester.pumpAndSettle();
final context = tester.element(find.byType(TcpScreen));
final l10n = AppLocalizations.of(context);
await tester.enterText(find.byType(TextField).first, '');
await tester.tap(find.byKey(const Key('tcp_connect_button')));
await tester.pumpAndSettle();
expect(find.text(l10n.tcpErrorHostRequired), findsOneWidget);
expect(connector.connectTcpCalls, 0);
await tester.enterText(find.byType(TextField).first, '192.168.1.50');
await tester.enterText(find.byType(TextField).at(1), '99999');
await tester.tap(find.byKey(const Key('tcp_connect_button')));
await tester.pumpAndSettle();
expect(connector.connectTcpCalls, 0);
});
testWidgets('TCP Bluetooth action returns to existing scanner route', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const ScannerScreen()),
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FloatingActionButton, 'TCP'));
await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsOneWidget);
await tester.tap(find.widgetWithText(FloatingActionButton, 'Bluetooth'));
await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsNothing);
expect(find.byType(ScannerScreen), findsOneWidget);
final navigatorState = tester.state<NavigatorState>(find.byType(Navigator));
expect(navigatorState.canPop(), isFalse);
// ScannerScreen.dispose() schedules disconnect work that debounces notify.
// Drain that debounce timer before test teardown.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
});
testWidgets('TcpScreen disables connect button while connector is scanning', (
tester,
) async {
final connector = _FakeMeshCoreConnector()
..initialState = MeshCoreConnectionState.scanning;
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
),
);
await tester.pumpAndSettle();
final button = tester.widget<ButtonStyleButton>(
find.byKey(const Key('tcp_connect_button')),
);
expect(button.onPressed, isNull);
expect(connector.connectTcpCalls, 0);
});
testWidgets('TcpScreen narrow width long status text does not overflow', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(320, 700));
addTearDown(() => tester.binding.setSurfaceSize(null));
final connector = _FakeMeshCoreConnector()
..initialState = MeshCoreConnectionState.connected
..initialTransport = MeshCoreTransportType.tcp
..initialEndpoint = 'meshcore-room-server-very-long-hostname.local:5000';
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const TcpScreen(),
locale: const Locale('en'),
), ),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
expect(find.text(longText), findsOneWidget);
final context = tester.element(find.byType(TcpScreen));
final l10n = AppLocalizations.of(context);
expect(
find.text(l10n.scanner_connectedTo(connector.initialEndpoint!)),
findsOneWidget,
);
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
}); });
} }
+221 -529
View File
@@ -1,584 +1,276 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/l10n/app_localizations.dart'; import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/utils/usb_port_labels.dart'; import 'package:meshcore_open/screens/scanner_screen.dart';
import 'package:meshcore_open/screens/usb_screen.dart';
import 'package:meshcore_open/utils/platform_info.dart';
// --------------------------------------------------------------------------- class _FakeMeshCoreConnector extends MeshCoreConnector {
// Pure helpers extracted from UsbScreen logic. _FakeMeshCoreConnector({
// --------------------------------------------------------------------------- this.initialState = MeshCoreConnectionState.disconnected,
List<String>? ports,
}) : _ports = ports ?? <String>[];
/// Mirrors `_UsbScreenState._buildStatusBar` text selection. final MeshCoreConnectionState initialState;
/// final List<String> _ports;
/// [isLoadingPorts] corresponds to the screen's `_isLoadingPorts` flag.
String usbStatusText({ String? requestPortLabel;
required bool isLoadingPorts, String? fallbackDeviceName;
required bool isUsbTransportConnected, int connectUsbCalls = 0;
required MeshCoreConnectionState state, String? lastConnectPortName;
required MeshCoreTransportType transport, String? fakeActiveUsbPort;
String? activeUsbPortDisplayLabel, String? fakeActiveUsbPortDisplayLabel;
// L10n strings passed directly so we don't need BuildContext. bool fakeUsbTransportConnected = false;
required String searching, Future<List<String>> Function()? listUsbPortsImpl;
required String Function(String) connectedTo, Future<void> Function({required String portName})? connectUsbImpl;
required String disconnecting,
required String connecting, @override
required String notConnected, MeshCoreConnectionState get state => initialState;
@override
MeshCoreTransportType get activeTransport => MeshCoreTransportType.usb;
@override
String? get activeUsbPort => fakeActiveUsbPort;
@override
String? get activeUsbPortDisplayLabel =>
fakeActiveUsbPortDisplayLabel ?? fakeActiveUsbPort;
@override
bool get isUsbTransportConnected => fakeUsbTransportConnected;
@override
Future<List<String>> listUsbPorts() async {
if (listUsbPortsImpl != null) {
return listUsbPortsImpl!();
}
return List<String>.from(_ports);
}
@override
Future<void> connectUsb({
required String portName,
int baudRate = 115200,
}) async {
if (connectUsbImpl != null) {
return connectUsbImpl!(portName: portName);
}
connectUsbCalls += 1;
lastConnectPortName = portName;
}
@override
void setUsbRequestPortLabel(String label) {
requestPortLabel = label;
}
@override
void setUsbFallbackDeviceName(String label) {
fallbackDeviceName = label;
}
}
Widget _buildTestApp({
required MeshCoreConnector connector,
required Widget child,
}) { }) {
if (isLoadingPorts) return searching; return ChangeNotifierProvider<MeshCoreConnector>.value(
if (isUsbTransportConnected) { value: connector,
switch (state) { child: MaterialApp(
case MeshCoreConnectionState.connected:
return connectedTo(activeUsbPortDisplayLabel ?? 'USB');
case MeshCoreConnectionState.disconnecting:
return disconnecting;
default:
return notConnected;
}
}
if (state == MeshCoreConnectionState.connecting &&
transport == MeshCoreTransportType.usb) {
return connecting;
}
return notConnected;
}
/// Mirrors `_UsbScreenState._friendlyErrorMessage`.
///
/// Uses string keys instead of l10n objects so this is a pure function.
String usbFriendlyErrorKey(Object error) {
if (error is PlatformException) {
switch (error.code) {
case 'usb_permission_denied':
return 'permissionDenied';
case 'usb_device_missing':
case 'usb_device_detached':
return 'deviceMissing';
case 'usb_invalid_port':
return 'invalidPort';
case 'usb_busy':
return 'busy';
case 'usb_not_connected':
return 'notConnected';
case 'usb_open_failed':
case 'usb_driver_missing':
return 'openFailed';
case 'usb_connect_failed':
return 'connectFailed';
}
}
if (error is UnsupportedError) return 'unsupported';
if (error is StateError) {
final msg = error.message;
if (msg.contains('already active')) return 'alreadyActive';
if (msg.contains('No USB serial device selected')) {
return 'noDeviceSelected';
}
if (msg.contains('not open') || msg.contains('closed')) {
return 'portClosed';
}
if (msg.contains('Timed out')) return 'connectTimedOut';
if (msg.contains('Failed to open')) return 'openFailed';
}
if (error is TimeoutException) return 'connectTimedOut';
return 'unknown';
}
/// Mirrors the guard in `_UsbScreenState._connectPort`:
/// returns true only when the connector is disconnected.
bool shouldAllowUsbConnect(MeshCoreConnectionState state) =>
state == MeshCoreConnectionState.disconnected;
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
// -- Port name helpers (normalizeUsbPortName / friendlyUsbPortName) -------
group('USB port name parsing', () {
test('normalizeUsbPortName extracts raw port before separator', () {
expect(normalizeUsbPortName('COM6 - USB Serial Device (COM6)'), 'COM6');
});
test('normalizeUsbPortName returns input when no separator', () {
expect(normalizeUsbPortName('/dev/ttyUSB0'), '/dev/ttyUSB0');
});
test('normalizeUsbPortName trims whitespace', () {
expect(normalizeUsbPortName(' COM3 '), 'COM3');
});
test('friendlyUsbPortName extracts description field', () {
expect(
friendlyUsbPortName('COM6 - USB Serial Device (COM6) - HWID'),
'USB Serial Device (COM6)',
);
});
test(
'friendlyUsbPortName falls back to raw name if description is n/a',
() {
expect(friendlyUsbPortName('COM6 - n/a'), 'COM6');
},
);
test('friendlyUsbPortName falls back when only one part', () {
expect(friendlyUsbPortName('/dev/ttyUSB0'), '/dev/ttyUSB0');
});
});
// -- Connect guard --------------------------------------------------------
group('USB connect guard', () {
test('allows connect when disconnected', () {
expect(
shouldAllowUsbConnect(MeshCoreConnectionState.disconnected),
isTrue,
);
});
test('blocks connect when connected', () {
expect(shouldAllowUsbConnect(MeshCoreConnectionState.connected), isFalse);
});
test('blocks connect when connecting', () {
expect(
shouldAllowUsbConnect(MeshCoreConnectionState.connecting),
isFalse,
);
});
test('blocks connect when scanning', () {
expect(shouldAllowUsbConnect(MeshCoreConnectionState.scanning), isFalse);
});
test('blocks connect when disconnecting', () {
expect(
shouldAllowUsbConnect(MeshCoreConnectionState.disconnecting),
isFalse,
);
});
});
// -- Status text ----------------------------------------------------------
group('USB status text', () {
String status({
bool isLoadingPorts = false,
bool isUsbTransportConnected = false,
MeshCoreConnectionState state = MeshCoreConnectionState.disconnected,
MeshCoreTransportType transport = MeshCoreTransportType.usb,
String? activeUsbPortDisplayLabel,
}) => usbStatusText(
isLoadingPorts: isLoadingPorts,
isUsbTransportConnected: isUsbTransportConnected,
state: state,
transport: transport,
activeUsbPortDisplayLabel: activeUsbPortDisplayLabel,
searching: 'SEARCHING',
connectedTo: (label) => 'CONNECTED:$label',
disconnecting: 'DISCONNECTING',
connecting: 'CONNECTING',
notConnected: 'NOT_CONNECTED',
);
test('loading ports shows searching', () {
expect(status(isLoadingPorts: true), 'SEARCHING');
});
test('connected USB with label', () {
expect(
status(
isUsbTransportConnected: true,
state: MeshCoreConnectionState.connected,
activeUsbPortDisplayLabel: 'COM6 - Device',
),
'CONNECTED:COM6 - Device',
);
});
test('connected USB with null label falls back to USB', () {
expect(
status(
isUsbTransportConnected: true,
state: MeshCoreConnectionState.connected,
),
'CONNECTED:USB',
);
});
test('USB transport connected but disconnecting', () {
expect(
status(
isUsbTransportConnected: true,
state: MeshCoreConnectionState.disconnecting,
),
'DISCONNECTING',
);
});
test('USB transport connected but scanning falls to default', () {
expect(
status(
isUsbTransportConnected: true,
state: MeshCoreConnectionState.scanning,
),
'NOT_CONNECTED',
);
});
test('connecting over USB shows connecting', () {
expect(status(state: MeshCoreConnectionState.connecting), 'CONNECTING');
});
test('connecting over bluetooth falls through to not-connected', () {
expect(
status(
state: MeshCoreConnectionState.connecting,
transport: MeshCoreTransportType.bluetooth,
),
'NOT_CONNECTED',
);
});
test('disconnected shows not-connected', () {
expect(status(), 'NOT_CONNECTED');
});
});
// -- Error mapping --------------------------------------------------------
group('USB friendly error mapping', () {
test('PlatformException usb_permission_denied', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_permission_denied')),
'permissionDenied',
);
});
test('PlatformException usb_device_missing', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_device_missing')),
'deviceMissing',
);
});
test('PlatformException usb_device_detached', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_device_detached')),
'deviceMissing',
);
});
test('PlatformException usb_invalid_port', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_invalid_port')),
'invalidPort',
);
});
test('PlatformException usb_busy', () {
expect(usbFriendlyErrorKey(PlatformException(code: 'usb_busy')), 'busy');
});
test('PlatformException usb_not_connected', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_not_connected')),
'notConnected',
);
});
test('PlatformException usb_open_failed', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_open_failed')),
'openFailed',
);
});
test('PlatformException usb_driver_missing', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_driver_missing')),
'openFailed',
);
});
test('PlatformException usb_connect_failed', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_connect_failed')),
'connectFailed',
);
});
test('PlatformException with unknown code falls through', () {
expect(
usbFriendlyErrorKey(PlatformException(code: 'usb_whatever')),
'unknown',
);
});
test('UnsupportedError → unsupported', () {
expect(usbFriendlyErrorKey(UnsupportedError('nope')), 'unsupported');
});
test('StateError "already active" → alreadyActive', () {
expect(
usbFriendlyErrorKey(StateError('already active')),
'alreadyActive',
);
});
test('StateError "No USB serial device selected" → noDeviceSelected', () {
expect(
usbFriendlyErrorKey(StateError('No USB serial device selected')),
'noDeviceSelected',
);
});
test('StateError "not open" → portClosed', () {
expect(usbFriendlyErrorKey(StateError('port not open')), 'portClosed');
});
test('StateError "closed" → portClosed', () {
expect(
usbFriendlyErrorKey(StateError('connection closed')),
'portClosed',
);
});
test('StateError "Timed out" → connectTimedOut', () {
expect(
usbFriendlyErrorKey(StateError('Timed out waiting')),
'connectTimedOut',
);
});
test('StateError "Failed to open" → openFailed', () {
expect(
usbFriendlyErrorKey(StateError('Failed to open device')),
'openFailed',
);
});
test('TimeoutException → connectTimedOut', () {
expect(usbFriendlyErrorKey(TimeoutException('slow')), 'connectTimedOut');
});
test('generic error → unknown', () {
expect(usbFriendlyErrorKey(Exception('boom')), 'unknown');
});
});
// -- Localized strings resolve correctly ----------------------------------
testWidgets('English USB localizations resolve without error', (
tester,
) async {
late AppLocalizations l10n;
await tester.pumpWidget(
MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: Builder( home: child,
builder: (context) { ),
l10n = AppLocalizations.of(context); );
return const SizedBox.shrink(); }
void main() {
testWidgets('UsbScreen passes localized chooser label to connector', (
tester,
) async {
final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
expect(connector.requestPortLabel, 'Select a USB device');
});
testWidgets(
'UsbScreen does not call connectUsb when connector is not disconnected',
(tester) async {
final connector = _FakeMeshCoreConnector(
initialState: MeshCoreConnectionState.connected,
ports: <String>['COM6 - USB Serial Device (COM6)'],
);
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
);
await tester.pumpAndSettle();
await tester.tap(find.byType(ListTile).first);
await tester.pump();
expect(connector.connectUsbCalls, 0);
// UsbScreen.dispose() schedules disconnect work that debounces notify.
// Drain that debounce timer before test teardown.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
}, },
), );
),
testWidgets('UsbScreen sends raw port name when tapping Connect', (
tester,
) async {
final connector = _FakeMeshCoreConnector(
ports: <String>['COM6 - USB Serial Device (COM6)'],
);
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(l10n.usbScreenTitle, isNotEmpty); await tester.tap(find.byType(ListTile).first);
expect(l10n.usbScreenStatus, 'Select a USB device'); await tester.pump();
expect(l10n.usbStatus_notConnected, isNotEmpty);
expect(l10n.usbStatus_connecting, isNotEmpty); expect(connector.connectUsbCalls, 1);
expect(l10n.usbStatus_searching, isNotEmpty); expect(connector.lastConnectPortName, 'COM6');
expect(l10n.usbErrorPermissionDenied, isNotEmpty);
expect(l10n.usbErrorDeviceMissing, isNotEmpty);
expect(l10n.usbErrorInvalidPort, isNotEmpty);
expect(l10n.usbErrorBusy, isNotEmpty);
expect(l10n.usbErrorNotConnected, isNotEmpty);
expect(l10n.usbErrorOpenFailed, isNotEmpty);
expect(l10n.usbErrorConnectFailed, isNotEmpty);
expect(l10n.usbErrorUnsupported, isNotEmpty);
expect(l10n.usbErrorAlreadyActive, isNotEmpty);
expect(l10n.usbErrorNoDeviceSelected, isNotEmpty);
expect(l10n.usbErrorPortClosed, isNotEmpty);
expect(l10n.usbErrorConnectTimedOut, isNotEmpty);
expect(l10n.scanner_connectedTo('device'), contains('device'));
expect(l10n.scanner_disconnecting, isNotEmpty);
}); });
// -- Isolated widget: status bar Row with FittedBox overflow -------------- testWidgets('ScannerScreen USB action reflects platform support', (
testWidgets('USB status bar with long text does not overflow at 320px', (
tester, tester,
) async { ) async {
await tester.binding.setSurfaceSize(const Size(320, 100)); final connector = _FakeMeshCoreConnector();
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const ScannerScreen()),
);
await tester.pumpAndSettle();
if (PlatformInfo.supportsUsbSerial) {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget);
} else {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing);
}
// ScannerScreen.dispose() schedules disconnect work that debounces notify.
// Drain that debounce timer before test teardown.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
});
testWidgets('ScannerScreen narrow width keeps actions without overflow', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(320, 700));
addTearDown(() => tester.binding.setSurfaceSize(null)); addTearDown(() => tester.binding.setSurfaceSize(null));
const longText = final connector = _FakeMeshCoreConnector();
'Connected to /dev/bus/usb/001/002 - KD3CGK mesh-utility.org very long label';
const statusColor = Colors.green;
// Exact widget tree from _buildStatusBar in UsbScreen.
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( _buildTestApp(connector: connector, child: const ScannerScreen()),
home: Scaffold(
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: statusColor.withValues(alpha: 0.1),
child: Row(
children: [
const Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
longText,
style: const TextStyle(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
),
),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
expect(find.text(longText), findsOneWidget);
final context = tester.element(find.byType(ScannerScreen));
final l10n = AppLocalizations.of(context);
expect(find.text(l10n.scanner_scan), findsOneWidget);
if (PlatformInfo.supportsUsbSerial) {
expect(find.text(l10n.connectionChoiceUsbLabel), findsOneWidget);
}
if (!PlatformInfo.isWeb) {
expect(find.text(l10n.connectionChoiceTcpLabel), findsOneWidget);
}
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
}); });
// -- Isolated widget: bottom nav FittedBox overflow ----------------------- testWidgets('UsbScreen narrow width long status text does not overflow', (
testWidgets('Bottom nav row with multiple FABs does not overflow at 320px', (
tester, tester,
) async { ) async {
await tester.binding.setSurfaceSize(const Size(320, 200)); await tester.binding.setSurfaceSize(const Size(320, 700));
addTearDown(() => tester.binding.setSurfaceSize(null)); addTearDown(() => tester.binding.setSurfaceSize(null));
// Mirrors the bottomNavigationBar structure from ScannerScreen / UsbScreen final connector =
// with all possible buttons visible. _FakeMeshCoreConnector(initialState: MeshCoreConnectionState.connected)
..fakeUsbTransportConnected = true
..fakeActiveUsbPortDisplayLabel =
'/dev/bus/usb/001/002 - KD3CGK mesh-utility.org very long label';
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( _buildTestApp(connector: connector, child: const UsbScreen()),
home: Scaffold(
body: const SizedBox.expand(),
bottomNavigationBar: SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.extended(
onPressed: () {},
heroTag: 'usb',
icon: const Icon(Icons.usb),
label: const Text('USB'),
),
const SizedBox(width: 12),
FloatingActionButton.extended(
onPressed: () {},
heroTag: 'tcp',
icon: const Icon(Icons.lan),
label: const Text('TCP'),
),
const SizedBox(width: 12),
FloatingActionButton.extended(
onPressed: () {},
heroTag: 'ble',
icon: const Icon(Icons.bluetooth_searching),
label: const Text('Scan'),
),
],
),
),
),
),
),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
expect(find.text('USB'), findsOneWidget);
expect(find.text('TCP'), findsOneWidget);
expect(find.text('Scan'), findsOneWidget);
});
// -- describeWebUsbPort --------------------------------------------------- final context = tester.element(find.byType(UsbScreen));
final l10n = AppLocalizations.of(context);
group('describeWebUsbPort', () {
test('null vendor and product returns requestPortLabel', () {
expect( expect(
describeWebUsbPort(vendorId: null, productId: null), find.text(
'Choose USB Device', l10n.scanner_connectedTo(connector.fakeActiveUsbPortDisplayLabel!),
);
});
test('known VID:PID uses knownUsbNames', () {
expect(
describeWebUsbPort(
vendorId: 0x1A86,
productId: 0x7523,
knownUsbNames: {'1a86:7523': 'CH340 Serial'},
), ),
'CH340 Serial (VID:1A86 PID:7523)', findsOneWidget,
); );
await tester.pumpWidget(const SizedBox.shrink());
await tester.pump(const Duration(milliseconds: 60));
}); });
test('unknown VID:PID uses fallback device name', () { group('Error Handling', () {
expect( testWidgets('shows error SnackBar when listing ports fails', (
describeWebUsbPort( tester,
vendorId: 0x1234, ) async {
productId: 0x5678, final connector = _FakeMeshCoreConnector();
fallbackDeviceName: 'My Device', connector.listUsbPortsImpl = () async {
), throw PlatformException(
'My Device (VID:1234 PID:5678)', code: 'usb_permission_denied',
message: 'Permission denied',
); );
}); };
});
// -- buildUsbDisplayLabel ------------------------------------------------- await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
group('buildUsbDisplayLabel', () {
test('appends device name when present', () {
expect(
buildUsbDisplayLabel(
basePortLabel: 'COM6',
deviceName: 'MeshCore Node',
),
'COM6 - MeshCore Node',
); );
await tester.pumpAndSettle();
expect(find.text('USB permission was denied.'), findsOneWidget);
}); });
test('returns base label when device name is null', () { testWidgets('connection failure shows SnackBar error', (tester) async {
expect( final connector = _FakeMeshCoreConnector(ports: <String>['COM1']);
buildUsbDisplayLabel(basePortLabel: 'COM6', deviceName: null), var connectAttempted = false;
'COM6', connector.connectUsbImpl = ({required String portName}) async {
connectAttempted = true;
throw PlatformException(code: 'usb_busy', message: 'Device is busy');
};
await tester.pumpWidget(
_buildTestApp(connector: connector, child: const UsbScreen()),
); );
}); await tester.pumpAndSettle();
test('returns base label when device name is whitespace', () { await tester.tap(find.byType(ListTile).first);
await tester.pumpAndSettle();
expect(connectAttempted, isTrue);
expect( expect(
buildUsbDisplayLabel(basePortLabel: 'COM6', deviceName: ' '), find.text('Another USB connection request is already in progress.'),
'COM6', findsOneWidget,
); );
}); });
}); });
+1 -69
View File
@@ -1,69 +1 @@
{ {}
"bg": [
"chat_sendMessage"
],
"de": [
"chat_sendMessage"
],
"es": [
"chat_sendMessage"
],
"fr": [
"chat_sendMessage"
],
"hu": [
"chat_sendMessage"
],
"it": [
"chat_sendMessage"
],
"ja": [
"chat_sendMessage"
],
"ko": [
"chat_sendMessage"
],
"nl": [
"chat_sendMessage"
],
"pl": [
"chat_sendMessage"
],
"pt": [
"chat_sendMessage"
],
"ru": [
"chat_sendMessage"
],
"sk": [
"chat_sendMessage"
],
"sl": [
"chat_sendMessage"
],
"sv": [
"chat_sendMessage"
],
"uk": [
"chat_sendMessage"
],
"zh": [
"chat_sendMessage"
]
}