mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-05 23:42:57 +10:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e53c493e78 | |||
| 54e0dae172 | |||
| 066aba7c5d | |||
| 6b6a881c7a | |||
| 8ef8a38495 | |||
| ddcda4ba5a | |||
| b572314ae9 | |||
| e97fb9bd24 | |||
| a4bbeffddc | |||
| 37ec8f2f05 | |||
| 39cd6d5514 | |||
| 44eb4fad58 | |||
| 1a209cbcfc | |||
| 33a8f34463 | |||
| ce8e8f0d5b | |||
| aa2d0f1927 | |||
| 0757c8e53a | |||
| add4731d05 | |||
| 7dc162d968 | |||
| 8ba4bbfbc5 | |||
| cac6abfef1 | |||
| bdd7fc0cdd |
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 => 'Статус на повтарянето';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 => '再送ステータス';
|
||||||
|
|
||||||
|
|||||||
@@ -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 => '반복 장치 상태';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 => 'Статус репитера';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 => 'Статус ретранслятора';
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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": "Bluetooth‑koppelings‑PIN",
|
"scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN",
|
||||||
"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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 (0–1) at which the counter turns the warning colour (default 0.7).
|
||||||
|
final double warningThreshold;
|
||||||
|
|
||||||
|
/// Ratio (0–1) 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user