mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
Merge remote-tracking branch 'origin/dev' into gps-toggle-in-settings
# Conflicts: # lib/l10n/app_bg.arb # lib/l10n/app_de.arb # lib/l10n/app_es.arb # lib/l10n/app_fr.arb # lib/l10n/app_hu.arb # lib/l10n/app_it.arb # lib/l10n/app_ja.arb # lib/l10n/app_ko.arb # lib/l10n/app_localizations_es.dart # lib/l10n/app_localizations_it.dart # lib/l10n/app_localizations_nl.dart # lib/l10n/app_localizations_pt.dart # lib/l10n/app_localizations_sv.dart # lib/l10n/app_localizations_uk.dart # lib/l10n/app_nl.arb # lib/l10n/app_pl.arb # lib/l10n/app_pt.arb # lib/l10n/app_ru.arb # lib/l10n/app_sk.arb # lib/l10n/app_sl.arb # lib/l10n/app_sv.arb # lib/l10n/app_uk.arb # lib/l10n/app_zh.arb
This commit is contained in:
@@ -87,6 +87,7 @@ keystore.properties
|
||||
# IDE
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
.contextstream/
|
||||
|
||||
# Cloudflare Wrangler
|
||||
.wrangler
|
||||
|
||||
@@ -675,6 +675,27 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void setContactUnreadCount(String contactKeyHex, int count) {
|
||||
_contactUnreadCount[contactKeyHex] = count;
|
||||
_unreadStore.saveContactUnreadCount(
|
||||
Map<String, int>.from(_contactUnreadCount),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setChannelUnreadCount(int channelIndex, int count) {
|
||||
final channel = _findChannelByIndex(channelIndex);
|
||||
if (channel != null) {
|
||||
channel.unreadCount = count;
|
||||
unawaited(
|
||||
_channelStore.saveChannels(
|
||||
_channels.isNotEmpty ? _channels : _cachedChannels,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void markChannelRead(int channelIndex) {
|
||||
final channel = _findChannelByIndex(channelIndex);
|
||||
if (channel != null && channel.unreadCount > 0) {
|
||||
@@ -2157,6 +2178,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_bleInitialSyncStarted = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
@@ -3030,13 +3052,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_pendingChannelSentQueue.add(message.messageId);
|
||||
notifyListeners();
|
||||
|
||||
final trimmed = text.trim();
|
||||
final isStructuredPayload =
|
||||
trimmed.startsWith('g:') || trimmed.startsWith('m:');
|
||||
final outboundText =
|
||||
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||
? Smaz.encodeIfSmaller(text)
|
||||
: text;
|
||||
final outboundText = prepareChannelOutboundText(channel.index, text);
|
||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||
@@ -4046,7 +4062,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
} else {
|
||||
appLogger.info(
|
||||
"Discovered contact ${contact.name} (type ${contact.typeLabel}) not added due to auto-add settings",
|
||||
"Discovered contact ${contact.name} (type ${contact.typeLabelRaw}) not added due to auto-add settings",
|
||||
tag: 'Connector',
|
||||
);
|
||||
return;
|
||||
@@ -4068,7 +4084,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactType: contact.typeLabelRaw,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
@@ -4143,7 +4159,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactType: contact.typeLabelRaw,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
@@ -4166,7 +4182,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (_contacts.isEmpty) return 0;
|
||||
var latest = 0;
|
||||
for (final contact in _contacts) {
|
||||
final seconds = contact.lastSeen.millisecondsSinceEpoch ~/ 1000;
|
||||
// prefer lastmod per spec, fallback to lastseen
|
||||
final source = contact.lastModified ?? contact.lastSeen;
|
||||
final seconds = source.millisecondsSinceEpoch ~/ 1000;
|
||||
if (seconds > latest) {
|
||||
latest = seconds;
|
||||
}
|
||||
@@ -4495,6 +4513,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
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) {
|
||||
for (final channel in _channels) {
|
||||
if (channel.index != channelIndex) continue;
|
||||
@@ -6090,7 +6118,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactType: contact.typeLabelRaw,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ const int maxPathSize = 64;
|
||||
const int pathHashSize = 1;
|
||||
const int maxNameSize = 32;
|
||||
const int maxFrameSize = 172;
|
||||
const int appProtocolVersion = 3;
|
||||
const int appProtocolVersion = 4;
|
||||
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
||||
const int maxTextPayloadBytes = 160;
|
||||
const int _sendTextMsgOverheadBytes =
|
||||
@@ -720,25 +720,19 @@ Uint8List buildUpdateContactPathFrame(
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(timestamp);
|
||||
|
||||
if ((lat == null || lon == null) && lastModified != null) {
|
||||
// If lat/lon not provided, write zeros
|
||||
writer.writeInt32LE(0);
|
||||
writer.writeInt32LE(0);
|
||||
} else {
|
||||
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
|
||||
// Latitude
|
||||
final latitude = lat ?? 0.0;
|
||||
writer.writeInt32LE((latitude * 1e6).round());
|
||||
|
||||
// Longitude
|
||||
final longitude = lon ?? 0.0;
|
||||
writer.writeInt32LE((longitude * 1e6).round());
|
||||
}
|
||||
|
||||
if (lastModified != null) {
|
||||
// Last modified
|
||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||
// Optional [Lat x4, Lon x4][timestamp x4] tail per the doc comment above.
|
||||
// Emit 8 bytes of position (zero-filled when only lastModified is provided)
|
||||
// followed by an optional 4-byte timestamp. Earlier code emitted the
|
||||
// position block twice, which corrupted the tail and caused the firmware
|
||||
// to parse the second lat as the timestamp. See #427.
|
||||
final hasLocation = lat != null && lon != null;
|
||||
if (hasLocation || lastModified != null) {
|
||||
writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0);
|
||||
writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0);
|
||||
if (lastModified != null) {
|
||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
return writer.toBytes();
|
||||
|
||||
@@ -49,6 +49,25 @@ class ChatScrollController extends ScrollController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Jumps toward an off-screen message so that lazy ListView.builder builds
|
||||
/// items near it. Only visible + cacheExtent items have real heights, so we
|
||||
/// use proportion of maxScrollExtent (itself an estimate from built items'
|
||||
/// avg height). Call [onJumped] on the next frame to ensureVisible/scroll
|
||||
/// to the exact target.
|
||||
void jumpToEstimatedOffset({
|
||||
required int unreadCount,
|
||||
required int totalMessages,
|
||||
required VoidCallback onJumped,
|
||||
}) {
|
||||
if (!hasClients || totalMessages == 0) return;
|
||||
final maxExtent = position.maxScrollExtent;
|
||||
final jumpOffset = maxExtent * (unreadCount / totalMessages);
|
||||
if (jumpOffset > 100) {
|
||||
jumpTo(jumpOffset);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onJumped());
|
||||
}
|
||||
|
||||
void scrollToBottomIfAtBottom() {
|
||||
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
||||
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
||||
|
||||
@@ -4,8 +4,14 @@ import 'package:flutter/services.dart';
|
||||
|
||||
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
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
|
||||
TextEditingValue formatEditUpdate(
|
||||
@@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (maxBytes <= 0) return oldValue;
|
||||
final bytes = utf8.encode(newValue.text);
|
||||
if (bytes.length <= maxBytes) return newValue;
|
||||
if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue;
|
||||
|
||||
final truncated = _truncateToMaxBytes(newValue.text, maxBytes);
|
||||
return TextEditingValue(
|
||||
@@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
}
|
||||
|
||||
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();
|
||||
var used = 0;
|
||||
for (final rune in text.runes) {
|
||||
|
||||
+85
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Режим на поверителност е активиран",
|
||||
"settings_privacyModeDisabled": "Режим на поверителност е деактивиран",
|
||||
"settings_actions": "Действия",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Изпрати Реклама",
|
||||
"settings_sendAdvertisementSubtitle": "Сега присъствие в ефир",
|
||||
"settings_advertisementSent": "Реклама изпратена",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
|
||||
"contact_teleLoc": "Местоположение на телеметрията",
|
||||
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
|
||||
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
|
||||
@@ -2013,7 +2008,6 @@
|
||||
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Мулти-потвърди: {value}",
|
||||
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
|
||||
"map_showOverlaps": "Покриване на ключа на повтаряча",
|
||||
"map_runTraceWithReturnPath": "Върни се по същия път.",
|
||||
@@ -2217,5 +2211,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Списва региони, които забраняват движението по пътищата при наводнения.",
|
||||
"repeater_cliHelpStatsPackets": "(Само за серия) Показва статистически данни на ниво пакет.",
|
||||
"repeater_cliHelpStatsRadio": "(Само за конкретен сериал) Показва радиостатистика.",
|
||||
"repeater_cliHelpStatsCore": "(Само за серийния номер) Показва основните статистически данни за фърмуера."
|
||||
"repeater_cliHelpStatsCore": "(Само за серийния номер) Показва основните статистически данни за фърмуера.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Множество потвърждения",
|
||||
"map_sharedAt": "Споделено",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Избрано препятствие",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsHint": "Кликнете върху блокираната точка, за да я отбележите на картата.",
|
||||
"losBlockedSpotsTitle": "Ограничени места",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
|
||||
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
|
||||
"settings_actions": "Aktionen",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Sende Ankündigung",
|
||||
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
|
||||
"settings_advertisementSent": "Ankündigung gesendet",
|
||||
@@ -2023,13 +2025,6 @@
|
||||
"contact_lastSeen": "Zuletzt gesehen",
|
||||
"contact_clearChat": "Chat löschen",
|
||||
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
|
||||
"appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
|
||||
@@ -2042,7 +2037,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
|
||||
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
|
||||
"map_showOverlaps": "Überlappungen der Repeater-Taste",
|
||||
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2245,5 +2239,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Auflistung von Regionen, die den Verkehr aufgrund von Überschwemmungen verbieten.",
|
||||
"repeater_cliHelpStatsPackets": "(Nur für serielle Verbindungen) Zeigt Statistiken auf Paketebene.",
|
||||
"repeater_cliHelpStatsRadio": "(Nur für Serien) Zeigt Radiostatistiken an.",
|
||||
"repeater_cliHelpStatsCore": "(Nur für serielle Schnittstellen) Zeigt grundlegende Firmware-Statistiken."
|
||||
"repeater_cliHelpStatsCore": "(Nur für serielle Schnittstellen) Zeigt grundlegende Firmware-Statistiken.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Mehrere Bestätigungen",
|
||||
"map_sharedAt": "Geteilt",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Reservierte Plätze",
|
||||
"losSelectedObstructionTitle": "Ausgewählte Behinderung",
|
||||
"losBlockedSpotChip": "{distance} • {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsHint": "Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.",
|
||||
"losSelectedObstructionDetails": "Blockiert durch {obstruction} in einer Höhe von {heightUnit}, {distanceFromA} von A und {distanceFromB} von B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+94
-9
@@ -12,6 +12,7 @@
|
||||
"common_delete": "Delete",
|
||||
"common_deleteAll": "Delete All",
|
||||
"common_close": "Close",
|
||||
"common_done": "Done",
|
||||
"common_edit": "Edit",
|
||||
"common_add": "Add",
|
||||
"common_settings": "Settings",
|
||||
@@ -177,16 +178,11 @@
|
||||
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
|
||||
"settings_advertLocation": "Advert Location",
|
||||
"settings_advertLocationSubtitle": "Include location in advert.",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_multiAck": "Multi-ACKs",
|
||||
"settings_telemetryModeUpdated": "Telemetry mode updated",
|
||||
"settings_actions": "Actions",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Send Advertisement",
|
||||
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
|
||||
"settings_advertisementSent": "Advertisement sent",
|
||||
@@ -522,6 +518,14 @@
|
||||
},
|
||||
"channels_hashtagChannel": "Hashtag channel",
|
||||
"channels_public": "Public",
|
||||
"channels_via": "via {path}",
|
||||
"@channels_via": {
|
||||
"placeholders": {
|
||||
"path": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_private": "Private",
|
||||
"channels_publicChannel": "Public channel",
|
||||
"channels_privateChannel": "Private channel",
|
||||
@@ -764,6 +768,7 @@
|
||||
}
|
||||
},
|
||||
"chat_successes": "successes",
|
||||
"chat_score": "Score",
|
||||
"chat_removePath": "Remove path",
|
||||
"chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.",
|
||||
"chat_pathActions": "Path Actions:",
|
||||
@@ -815,6 +820,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_markAsUnread": "Mark as Unread",
|
||||
"chat_newMessages": "New messages",
|
||||
"chat_openLink": "Open Link?",
|
||||
"chat_openLinkConfirmation": "Do you want to open this link in your browser?",
|
||||
"chat_open": "Open",
|
||||
@@ -860,6 +867,12 @@
|
||||
"map_from": "From",
|
||||
"map_source": "Source",
|
||||
"map_flags": "Flags",
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"map_shareMarkerHere": "Share marker here",
|
||||
"map_setAsMyLocation": "Set as my location",
|
||||
"map_pinLabel": "Pin label",
|
||||
@@ -894,6 +907,7 @@
|
||||
"map_guessedLocation": "Guessed location",
|
||||
"map_lastSeenTime": "Last Seen Time",
|
||||
"map_sharedPin": "Shared pin",
|
||||
"map_sharedAt": "Shared",
|
||||
"map_joinRoom": "Join Room",
|
||||
"map_manageRepeater": "Manage Repeater",
|
||||
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
||||
@@ -2017,6 +2031,46 @@
|
||||
"losLegendRadioHorizon": "Radio horizon",
|
||||
"losLegendLosBeam": "LOS beam",
|
||||
"losLegendTerrain": "Terrain",
|
||||
"losBlockedSpotsTitle": "Blocked spots",
|
||||
"losBlockedSpotsHint": "Tap a blocked spot to highlight it on the map.",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Selected obstruction",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losFrequencyLabel": "Frequency",
|
||||
"losFrequencyInfoTooltip": "View calculation details",
|
||||
"losFrequencyDialogTitle": "Radio horizon calculation",
|
||||
@@ -2255,5 +2309,36 @@
|
||||
}
|
||||
},
|
||||
"translation_translationOptions": "Translation options",
|
||||
"translation_systemLanguage": "System language"
|
||||
"translation_systemLanguage": "System language",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown"
|
||||
}
|
||||
|
||||
+85
-8
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Modo de privacidad activado",
|
||||
"settings_privacyModeDisabled": "Modo de privacidad desactivado",
|
||||
"settings_actions": "Acciones",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Enviar Anuncio",
|
||||
"settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora",
|
||||
"settings_advertisementSent": "Anuncio enviado",
|
||||
@@ -2023,13 +2025,6 @@
|
||||
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
|
||||
"contact_teleEnv": "Entorno de Telemetría",
|
||||
"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_maxRouteWeight": "Peso máximo permitido para la ruta",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
|
||||
@@ -2245,5 +2240,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Enumera las regiones que prohíben el tráfico debido a las inundaciones.",
|
||||
"repeater_cliHelpStatsPackets": "(Solo para series) Muestra estadísticas a nivel de paquetes.",
|
||||
"repeater_cliHelpStatsRadio": "(Solo para transmisiones en serie) Muestra estadísticas de radio.",
|
||||
"repeater_cliHelpStatsCore": "(Solo para series) Muestra estadísticas clave del firmware."
|
||||
"repeater_cliHelpStatsCore": "(Solo para series) Muestra estadísticas clave del firmware.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Múltiples respuestas de confirmación",
|
||||
"map_sharedAt": "Compartido",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Espacios ocupados",
|
||||
"losBlockedSpotsHint": "Seleccione un punto bloqueado para resaltarlo en el mapa.",
|
||||
"losSelectedObstructionTitle": "Obstrucción seleccionada",
|
||||
"losSelectedObstructionDetails": "Bloqueado por {obstruction} a una altura de {heightUnit}, a {distanceFromA} metros de A y a {distanceFromB} metros de B ({distanceUnit}).",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||
}
|
||||
|
||||
+85
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Mode de confidentialité activé",
|
||||
"settings_privacyModeDisabled": "Mode de confidentialité désactivé",
|
||||
"settings_actions": "Actions",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "S'annoncer",
|
||||
"settings_sendAdvertisementSubtitle": "Présence diffusée maintenant",
|
||||
"settings_advertisementSent": "Annonce envoyée",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"contact_lastSeen": "Dernière fois vu",
|
||||
"contact_clearChat": "Effacer la conversation",
|
||||
"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_initialRouteWeight": "Poids initial de l'itinéraire",
|
||||
"appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
|
||||
@@ -2013,7 +2008,6 @@
|
||||
"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é.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Multi-ACKs : {value}",
|
||||
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
|
||||
"map_showOverlaps": "Chevauchement de la touche répétitive",
|
||||
"map_runTraceWithReturnPath": "Revenir sur le même chemin.",
|
||||
@@ -2224,5 +2218,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Liste des régions qui interdisent la circulation en cas de inondation.",
|
||||
"repeater_cliHelpStatsPackets": "(Uniquement pour les séries) Affiche des statistiques au niveau des paquets.",
|
||||
"repeater_cliHelpStatsRadio": "(Uniquement pour les séries) Affiche les statistiques de la radio.",
|
||||
"repeater_cliHelpStatsCore": "(Uniquement pour les séries) Affiche les statistiques du micrologicem intégré."
|
||||
"repeater_cliHelpStatsCore": "(Uniquement pour les séries) Affiche les statistiques du micrologicem intégré.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Plusieurs accusés de réception",
|
||||
"map_sharedAt": "Partagé",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Obstruction sélectionnée",
|
||||
"losBlockedSpotsTitle": "Places occupés",
|
||||
"losBlockedSpotsHint": "Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.",
|
||||
"losSelectedObstructionDetails": "Bloqué par {obstruction}, à une hauteur de {heightUnit}, à une distance de {distanceFromA} par rapport à A et à une distance de {distanceFromB} par rapport à B ({distanceUnit}).",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||
}
|
||||
|
||||
+85
-9
@@ -167,6 +167,8 @@
|
||||
"settings_privacyModeEnabled": "Adatvédelem mód beállítva",
|
||||
"settings_privacyModeDisabled": "Adatvédelem mód kikapcsolva",
|
||||
"settings_actions": "Tevékenységek",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Hirdetés küldése",
|
||||
"settings_sendAdvertisementSubtitle": "A nyilvános megjelenés",
|
||||
"settings_advertisementSent": "Hirdetés elküldve",
|
||||
@@ -2085,13 +2087,6 @@
|
||||
"radioStats_stripWaiting": "Rádió adatok begyűjtése…",
|
||||
"radioStats_settingsTile": "Rádió statisztikák",
|
||||
"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_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.",
|
||||
@@ -2103,7 +2098,6 @@
|
||||
"settings_telemetryEnvironmentMode": "Adatkapcsolati környezeti mód",
|
||||
"settings_advertLocation": "Reklám megjelenési hely",
|
||||
"settings_advertLocationSubtitle": "A hirdetés tartalmazza a helyszínt.",
|
||||
"settings_multiAck": "Többszöri visszaigazolások: {value}",
|
||||
"settings_telemetryModeUpdated": "A telemetriamód frissítve",
|
||||
"contact_info": "Kapcsolattartási információk",
|
||||
"contact_settings": "Kapcsolat beállítások",
|
||||
@@ -2255,5 +2249,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Felhasznál, amelyek elutasítják a árvíz okozta forgalmat.",
|
||||
"repeater_cliHelpStatsPackets": "(Csak sorozat) A csomagok szintjén történő statisztikát mutat.",
|
||||
"repeater_cliHelpStatsRadio": "(Csak sorozat) Mutat rádióstatisztikákat.",
|
||||
"repeater_cliHelpStatsCore": "(Csak soros mód) A főfirmware-adatokat mutatja."
|
||||
"repeater_cliHelpStatsCore": "(Csak soros mód) A főfirmware-adatokat mutatja.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Többszörös visszaigazolások",
|
||||
"map_sharedAt": "Megosztva",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Kiválasztott akadály",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsHint": "A blokkolt területet megjelölve, hogy a térképen kiemeljük.",
|
||||
"losBlockedSpotsTitle": "Foglalhatatlan területek",
|
||||
"losSelectedObstructionDetails": "Elakadt a {obstruction} miatt, {heightUnit} magasságban, {distanceFromA} méterrel A-tól és {distanceFromB} méterrel B-től ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-8
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Modalità privacy abilitata",
|
||||
"settings_privacyModeDisabled": "Modalità privacy disabilitata",
|
||||
"settings_actions": "Azioni",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Invia Annuncio",
|
||||
"settings_sendAdvertisementSubtitle": "Presenza trasmessa ora",
|
||||
"settings_advertisementSent": "Annuncio inviato",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"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_teleEnv": "Ambiente di telemetria",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso iniziale del percorso",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
|
||||
"appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
|
||||
@@ -2217,5 +2212,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Elenca le regioni che vietano il transito in caso di alluvioni.",
|
||||
"repeater_cliHelpStatsPackets": "(Solo per la visualizzazione dei dati seriali) Mostra statistiche a livello di pacchetto.",
|
||||
"repeater_cliHelpStatsRadio": "(Solo per serie TV) Visualizza statistiche relative alla trasmissione radiofonica.",
|
||||
"repeater_cliHelpStatsCore": "(Solo per serie) Visualizza le statistiche del firmware di base."
|
||||
"repeater_cliHelpStatsCore": "(Solo per serie) Visualizza le statistiche del firmware di base.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "ACK multipli",
|
||||
"map_sharedAt": "Condiviso",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Ostacolo selezionato",
|
||||
"losBlockedSpotsHint": "Tocca un punto bloccato sulla mappa per evidenziarlo.",
|
||||
"losBlockedSpotsTitle": "Posti occupati",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-9
@@ -167,6 +167,8 @@
|
||||
"settings_privacyModeEnabled": "プライバシーモードが有効になっています",
|
||||
"settings_privacyModeDisabled": "プライバシーモードは無効化されています",
|
||||
"settings_actions": "行動",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "広告を送信する",
|
||||
"settings_sendAdvertisementSubtitle": "現在、放送での活動",
|
||||
"settings_advertisementSent": "広告が送信されました",
|
||||
@@ -2085,13 +2087,6 @@
|
||||
"radioStats_stripWaiting": "ラジオの統計情報を取得中…",
|
||||
"radioStats_settingsTile": "ラジオの統計",
|
||||
"radioStats_settingsSubtitle": "ノイズレベル、RSSI、SNR、および通信時間",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "プライバシー設定",
|
||||
"settings_privacySubtitle": "共有する情報の内容を管理する。",
|
||||
"settings_denyAll": "すべてを否定",
|
||||
@@ -2103,7 +2098,6 @@
|
||||
"settings_telemetryEnvironmentMode": "テレメトリ環境モード",
|
||||
"settings_advertLocation": "広告掲載場所",
|
||||
"settings_advertLocationSubtitle": "広告に場所を記載してください。",
|
||||
"settings_multiAck": "複数のACK:{value}",
|
||||
"settings_telemetryModeUpdated": "テレメトリモードが更新されました",
|
||||
"contact_info": "連絡先",
|
||||
"contact_settings": "連絡設定",
|
||||
@@ -2255,5 +2249,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "洪水による交通を遮断している地域の一覧",
|
||||
"repeater_cliHelpStatsPackets": "(シリアルのみ)パケットレベルの統計情報を表示します。",
|
||||
"repeater_cliHelpStatsRadio": "(シリーズのみ)ラジオの統計情報を表示します。",
|
||||
"repeater_cliHelpStatsCore": "(シリアルのみ)主要なファームウェアの統計情報を表示します。"
|
||||
"repeater_cliHelpStatsCore": "(シリアルのみ)主要なファームウェアの統計情報を表示します。",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "複数のACK(応答)",
|
||||
"map_sharedAt": "共有済み",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "地図上で、特定された場所を強調するために、該当する場所をタップしてください。",
|
||||
"losSelectedObstructionTitle": "選択された障害",
|
||||
"losBlockedSpotsTitle": "利用できない場所",
|
||||
"losSelectedObstructionDetails": "{obstruction} によって {heightUnit} の高さで、A地点から {distanceFromA}、B地点から {distanceFromB} ({distanceUnit}) の距離で塞がれています。",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||
}
|
||||
|
||||
+85
-9
@@ -167,6 +167,8 @@
|
||||
"settings_privacyModeEnabled": "개인 정보 보호 모드 활성화",
|
||||
"settings_privacyModeDisabled": "개인 정보 보호 모드 비활성화",
|
||||
"settings_actions": "행동",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "광고 전송",
|
||||
"settings_sendAdvertisementSubtitle": "방송 활동",
|
||||
"settings_advertisementSent": "광고 전송",
|
||||
@@ -2085,13 +2087,6 @@
|
||||
"radioStats_stripWaiting": "라디오 통계 가져오기…",
|
||||
"radioStats_settingsTile": "라디오 통계",
|
||||
"radioStats_settingsSubtitle": "잡음 수준, RSSI, 신호 대 잡음비, 통신 시간",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "개인 정보 설정",
|
||||
"settings_privacySubtitle": "어떤 정보를 공유할지 통제하세요.",
|
||||
"settings_privacySettingsDescription": "어떤 정보를 기기가 다른 사람들과 공유할지 선택하세요.",
|
||||
@@ -2103,7 +2098,6 @@
|
||||
"settings_telemetryEnvironmentMode": "텔레메트리 환경 모드",
|
||||
"settings_advertLocation": "광고 위치",
|
||||
"settings_advertLocationSubtitle": "광고에 위치 정보를 포함하세요.",
|
||||
"settings_multiAck": "다중 ACK: {value}",
|
||||
"settings_telemetryModeUpdated": "텔레메트리 모드 업데이트 완료",
|
||||
"contact_info": "연락처",
|
||||
"contact_settings": "연락처 설정",
|
||||
@@ -2255,5 +2249,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "홍수 발생 시 통행 금지 지역 목록",
|
||||
"repeater_cliHelpStatsPackets": "(전송 속도만 표시) 패킷 수준의 통계 정보를 보여줍니다.",
|
||||
"repeater_cliHelpStatsRadio": "(특정 시리즈만 해당) 라디오 통계 정보를 표시합니다.",
|
||||
"repeater_cliHelpStatsCore": "(시리얼 번호만 표시) 핵심 펌웨어 통계 정보를 보여줍니다."
|
||||
"repeater_cliHelpStatsCore": "(시리얼 번호만 표시) 핵심 펌웨어 통계 정보를 보여줍니다.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "다중 ACK",
|
||||
"map_sharedAt": "공유됨",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.",
|
||||
"losBlockedSpotsTitle": "차단된 공간",
|
||||
"losSelectedObstructionTitle": "선택된 장애물",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
@@ -202,6 +202,12 @@ abstract class AppLocalizations {
|
||||
/// **'Close'**
|
||||
String get common_close;
|
||||
|
||||
/// No description provided for @common_done.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Done'**
|
||||
String get common_done;
|
||||
|
||||
/// No description provided for @common_edit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -901,8 +907,8 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @settings_multiAck.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Multi-ACKs: {value}'**
|
||||
String settings_multiAck(String value);
|
||||
/// **'Multi-ACKs'**
|
||||
String get settings_multiAck;
|
||||
|
||||
/// No description provided for @settings_telemetryModeUpdated.
|
||||
///
|
||||
@@ -916,6 +922,18 @@ abstract class AppLocalizations {
|
||||
/// **'Actions'**
|
||||
String get settings_actions;
|
||||
|
||||
/// No description provided for @settings_deleteAllPaths.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete All Paths'**
|
||||
String get settings_deleteAllPaths;
|
||||
|
||||
/// No description provided for @settings_deleteAllPathsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear all path data from contacts.'**
|
||||
String get settings_deleteAllPathsSubtitle;
|
||||
|
||||
/// No description provided for @settings_sendAdvertisement.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2044,6 +2062,12 @@ abstract class AppLocalizations {
|
||||
/// **'Public'**
|
||||
String get channels_public;
|
||||
|
||||
/// No description provided for @channels_via.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'via {path}'**
|
||||
String channels_via(String path);
|
||||
|
||||
/// No description provided for @channels_private.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2662,6 +2686,12 @@ abstract class AppLocalizations {
|
||||
/// **'successes'**
|
||||
String get chat_successes;
|
||||
|
||||
/// No description provided for @chat_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Score'**
|
||||
String get chat_score;
|
||||
|
||||
/// No description provided for @chat_removePath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2824,6 +2854,18 @@ abstract class AppLocalizations {
|
||||
/// **'Unread: {count}'**
|
||||
String chat_unread(int count);
|
||||
|
||||
/// No description provided for @chat_markAsUnread.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mark as Unread'**
|
||||
String get chat_markAsUnread;
|
||||
|
||||
/// No description provided for @chat_newMessages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New messages'**
|
||||
String get chat_newMessages;
|
||||
|
||||
/// No description provided for @chat_openLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2968,6 +3010,42 @@ abstract class AppLocalizations {
|
||||
/// **'Flags'**
|
||||
String get map_flags;
|
||||
|
||||
/// No description provided for @map_type.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Type'**
|
||||
String get map_type;
|
||||
|
||||
/// No description provided for @map_path.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path'**
|
||||
String get map_path;
|
||||
|
||||
/// No description provided for @map_location.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Location'**
|
||||
String get map_location;
|
||||
|
||||
/// No description provided for @map_estLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Est. Location'**
|
||||
String get map_estLocation;
|
||||
|
||||
/// No description provided for @map_publicKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Public Key'**
|
||||
String get map_publicKey;
|
||||
|
||||
/// No description provided for @map_publicKeyPrefixHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'e.g. ab12'**
|
||||
String get map_publicKeyPrefixHint;
|
||||
|
||||
/// No description provided for @map_shareMarkerHere.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3130,6 +3208,12 @@ abstract class AppLocalizations {
|
||||
/// **'Shared pin'**
|
||||
String get map_sharedPin;
|
||||
|
||||
/// No description provided for @map_sharedAt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Shared'**
|
||||
String get map_sharedAt;
|
||||
|
||||
/// No description provided for @map_joinRoom.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -6318,6 +6402,47 @@ abstract class AppLocalizations {
|
||||
/// **'Terrain'**
|
||||
String get losLegendTerrain;
|
||||
|
||||
/// No description provided for @losBlockedSpotsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Blocked spots'**
|
||||
String get losBlockedSpotsTitle;
|
||||
|
||||
/// No description provided for @losBlockedSpotsHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap a blocked spot to highlight it on the map.'**
|
||||
String get losBlockedSpotsHint;
|
||||
|
||||
/// No description provided for @losBlockedSpotChip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{distance} {distanceUnit} • {obstruction} {heightUnit}'**
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
);
|
||||
|
||||
/// No description provided for @losSelectedObstructionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Selected obstruction'**
|
||||
String get losSelectedObstructionTitle;
|
||||
|
||||
/// No description provided for @losSelectedObstructionDetails.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).'**
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
);
|
||||
|
||||
/// No description provided for @losFrequencyLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -7066,6 +7191,66 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'System language'**
|
||||
String get translation_systemLanguage;
|
||||
|
||||
/// No description provided for @background_serviceTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'MeshCore running'**
|
||||
String get background_serviceTitle;
|
||||
|
||||
/// No description provided for @background_serviceText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keeping BLE connected'**
|
||||
String get background_serviceText;
|
||||
|
||||
/// No description provided for @appSettings_translationModelDeleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Deleted {name}'**
|
||||
String appSettings_translationModelDeleted(String name);
|
||||
|
||||
/// No description provided for @appSettings_translationModelDeleteFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to delete: {error}'**
|
||||
String appSettings_translationModelDeleteFailed(String error);
|
||||
|
||||
/// No description provided for @channels_channelUpdateFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to update channel: {error}'**
|
||||
String channels_channelUpdateFailed(String error);
|
||||
|
||||
/// No description provided for @contact_typeChat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Chat'**
|
||||
String get contact_typeChat;
|
||||
|
||||
/// No description provided for @contact_typeRepeater.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Repeater'**
|
||||
String get contact_typeRepeater;
|
||||
|
||||
/// No description provided for @contact_typeRoom.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Room'**
|
||||
String get contact_typeRoom;
|
||||
|
||||
/// No description provided for @contact_typeSensor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sensor'**
|
||||
String get contact_typeSensor;
|
||||
|
||||
/// No description provided for @contact_typeUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unknown'**
|
||||
String get contact_typeUnknown;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Затвори';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Редактирай';
|
||||
|
||||
@@ -437,9 +440,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
'Включи местоположение в обявата';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мулти-потвърди: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Множество потвърждения';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
|
||||
@@ -447,6 +448,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Изпрати Реклама';
|
||||
|
||||
@@ -1097,6 +1105,11 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Публично';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Личен';
|
||||
|
||||
@@ -1458,6 +1471,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Успехи';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Премахни пътя';
|
||||
|
||||
@@ -1560,6 +1576,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Непрочетени: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Отваряне на връзката?';
|
||||
|
||||
@@ -1641,6 +1663,24 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Флаг';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Споделете маркер тук';
|
||||
|
||||
@@ -1726,6 +1766,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Споделено копие';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Споделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присъедини се към стаята';
|
||||
|
||||
@@ -3663,6 +3706,37 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Терен';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Ограничени места';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Кликнете върху блокираната точка, за да я отбележите на картата.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Избрано препятствие';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Честота';
|
||||
|
||||
@@ -4127,4 +4201,40 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Език на системата';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Schließen';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Bearbeiten';
|
||||
|
||||
@@ -435,9 +438,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Ort in der Anzeige einbeziehen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Mehrfach-Bestätigungen: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Mehrere Bestätigungen';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
|
||||
@@ -445,6 +446,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Aktionen';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Sende Ankündigung';
|
||||
|
||||
@@ -1092,6 +1100,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Öffentlich';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privat';
|
||||
|
||||
@@ -1457,6 +1470,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Erfolgreich';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Pfad entfernen';
|
||||
|
||||
@@ -1557,6 +1573,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Ungelesen: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Link öffnen?';
|
||||
|
||||
@@ -1638,6 +1660,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flaggen';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
|
||||
|
||||
@@ -1723,6 +1763,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Gemeinsames Passwort';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Geteilt';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Beitreten Sie dem Raum';
|
||||
|
||||
@@ -3674,6 +3717,37 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Gelände';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Reservierte Plätze';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance • $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Ausgewählte Behinderung';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blockiert durch $obstruction in einer Höhe von $heightUnit, $distanceFromA von A und $distanceFromB von B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequenz';
|
||||
|
||||
@@ -4144,4 +4218,40 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Sprache des Systems';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Close';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Edit';
|
||||
|
||||
@@ -427,9 +430,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Include location in advert.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
|
||||
@@ -437,6 +438,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Send Advertisement';
|
||||
|
||||
@@ -1074,6 +1082,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Public';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Private';
|
||||
|
||||
@@ -1430,6 +1443,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'successes';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Remove path';
|
||||
|
||||
@@ -1527,6 +1543,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Unread: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Open Link?';
|
||||
|
||||
@@ -1608,6 +1630,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flags';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Share marker here';
|
||||
|
||||
@@ -1692,6 +1732,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Shared pin';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Join Room';
|
||||
|
||||
@@ -3601,6 +3644,37 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrain';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Blocked spots';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Tap a blocked spot to highlight it on the map.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Selected obstruction';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequency';
|
||||
|
||||
@@ -4052,4 +4126,40 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'System language';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Cerrar';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Editar';
|
||||
|
||||
@@ -434,9 +437,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Múltiples respuestas de confirmación: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Múltiples respuestas de confirmación';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
|
||||
@@ -444,6 +445,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Acciones';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Enviar Anuncio';
|
||||
|
||||
@@ -1094,6 +1102,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Público';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privado';
|
||||
|
||||
@@ -1455,6 +1468,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Éxitos';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Eliminar ruta';
|
||||
|
||||
@@ -1556,6 +1572,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Sin leer: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => '¿Abrir enlace?';
|
||||
|
||||
@@ -1637,6 +1659,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Banderas';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Compartir marcador aquí';
|
||||
|
||||
@@ -1722,6 +1762,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Pin compartido';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Compartido';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Únete a la sala';
|
||||
|
||||
@@ -3663,6 +3706,37 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Espacios ocupados';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Seleccione un punto bloqueado para resaltarlo en el mapa.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Obstrucción seleccionada';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Bloqueado por $obstruction a una altura de $heightUnit, a $distanceFromA metros de A y a $distanceFromB metros de B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frecuencia';
|
||||
|
||||
@@ -4132,4 +4206,40 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Idioma del sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Fermer';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Modifier';
|
||||
|
||||
@@ -438,9 +441,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Inclure l\'emplacement dans l\'annonce';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs : $value';
|
||||
}
|
||||
String get settings_multiAck => 'Plusieurs accusés de réception';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
@@ -449,6 +450,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'S\'annoncer';
|
||||
|
||||
@@ -1099,6 +1107,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Public';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privé';
|
||||
|
||||
@@ -1462,6 +1475,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Succès';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Supprimer le chemin';
|
||||
|
||||
@@ -1565,6 +1581,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Non lu : $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Ouvrir le lien ?';
|
||||
|
||||
@@ -1647,6 +1669,24 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Drapeaux';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Partager le marqueur ici';
|
||||
|
||||
@@ -1732,6 +1772,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Clé partagée';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Partagé';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Rejoindre le room server';
|
||||
|
||||
@@ -3686,6 +3729,37 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrain';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Places occupés';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Obstruction sélectionnée';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Bloqué par $obstruction, à une hauteur de $heightUnit, à une distance de $distanceFromA par rapport à A et à une distance de $distanceFromB par rapport à B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Fréquence';
|
||||
|
||||
@@ -4161,4 +4235,40 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Langue du système';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Bezárás';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Szerkesztés';
|
||||
|
||||
@@ -437,9 +440,7 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
'A hirdetés tartalmazza a helyszínt.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Többszöri visszaigazolások: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Többszörös visszaigazolások';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'A telemetriamód frissítve';
|
||||
@@ -447,6 +448,13 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Tevékenységek';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Hirdetés küldése';
|
||||
|
||||
@@ -1099,6 +1107,11 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'A nyilvánosság számára';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Személyes';
|
||||
|
||||
@@ -1467,6 +1480,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'sikerek';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Törölje a elérési útvonalat';
|
||||
|
||||
@@ -1567,6 +1583,12 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
return 'Olvasatlan: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Nyisd meg a linket?';
|
||||
|
||||
@@ -1649,6 +1671,24 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Zászló';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Osztja ezt a tartalmat itt';
|
||||
|
||||
@@ -1735,6 +1775,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Gemeinsames PIN-kód';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Megosztva';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Csatlakozás a szobához';
|
||||
|
||||
@@ -3679,6 +3722,37 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terület';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Foglalhatatlan területek';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'A blokkolt területet megjelölve, hogy a térképen kiemeljük.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Kiválasztott akadály';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Elakadt a $obstruction miatt, $heightUnit magasságban, $distanceFromA méterrel A-tól és $distanceFromB méterrel B-től ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Hatósság';
|
||||
|
||||
@@ -4150,4 +4224,40 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Rendszer nyelvé';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Chiudi';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Modifica';
|
||||
|
||||
@@ -437,9 +440,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Includi la posizione nell\'annuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'ACK multipli: $value';
|
||||
}
|
||||
String get settings_multiAck => 'ACK multipli';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
|
||||
@@ -447,6 +448,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Azioni';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Invia Annuncio';
|
||||
|
||||
@@ -1095,6 +1103,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Pubblico';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privato';
|
||||
|
||||
@@ -1456,6 +1469,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'successi';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Rimuovi percorso';
|
||||
|
||||
@@ -1558,6 +1574,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Non letti: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Aprire il link?';
|
||||
|
||||
@@ -1639,6 +1661,24 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Bandiere';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Condividi marcatore qui';
|
||||
|
||||
@@ -1723,6 +1763,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Condividi PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Condiviso';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Unisciti alla stanza';
|
||||
|
||||
@@ -3667,6 +3710,37 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Posti occupati';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Tocca un punto bloccato sulla mappa per evidenziarlo.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Ostacolo selezionato';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequenza';
|
||||
|
||||
@@ -4136,4 +4210,40 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Lingua del sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => '閉じる';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => '編集';
|
||||
|
||||
@@ -414,9 +417,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '複数のACK:$value';
|
||||
}
|
||||
String get settings_multiAck => '複数のACK(応答)';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'テレメトリモードが更新されました';
|
||||
@@ -424,6 +425,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => '行動';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => '広告を送信する';
|
||||
|
||||
@@ -1041,6 +1049,11 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => '一般の人々';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => '個人の';
|
||||
|
||||
@@ -1394,6 +1407,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => '成功事例';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'パスを削除する';
|
||||
|
||||
@@ -1489,6 +1505,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '未読: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'リンクを開く?';
|
||||
|
||||
@@ -1567,6 +1589,24 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => '旗';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'この場所でシェア';
|
||||
|
||||
@@ -1650,6 +1690,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => '共有パスワード';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '共有済み';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '部屋に参加する';
|
||||
|
||||
@@ -3471,6 +3514,36 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => '地形';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => '利用できない場所';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint => '地図上で、特定された場所を強調するために、該当する場所をタップしてください。';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => '選択された障害';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return '$obstruction によって $heightUnit の高さで、A地点から $distanceFromA、B地点から $distanceFromB ($distanceUnit) の距離で塞がれています。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => '周波数';
|
||||
|
||||
@@ -3907,4 +3980,40 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'システム言語';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => '닫기';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => '수정';
|
||||
|
||||
@@ -414,9 +417,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '다중 ACK: $value';
|
||||
}
|
||||
String get settings_multiAck => '다중 ACK';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => '텔레메트리 모드 업데이트 완료';
|
||||
@@ -424,6 +425,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => '행동';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => '광고 전송';
|
||||
|
||||
@@ -1036,6 +1044,11 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => '대중의';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => '사립';
|
||||
|
||||
@@ -1390,6 +1403,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => '성공 사례';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => '경로 제거';
|
||||
|
||||
@@ -1485,6 +1501,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
return '읽지 않음: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => '링크를 열기?';
|
||||
|
||||
@@ -1563,6 +1585,24 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => '깃발';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => '여기에서 마커 공유';
|
||||
|
||||
@@ -1646,6 +1686,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => '공유 비밀번호';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '공유됨';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '방에 참여';
|
||||
|
||||
@@ -3473,6 +3516,36 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => '지형';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => '차단된 공간';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint => '지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => '선택된 장애물';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => '빈도';
|
||||
|
||||
@@ -3908,4 +3981,40 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => '시스템 언어';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Sluiten';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Bewerken';
|
||||
|
||||
@@ -432,9 +435,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Locatie opnemen in advertentie';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Meerdere bevestigingen: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Meerdere bevestigingen';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
|
||||
@@ -442,6 +443,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Acties';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Verzend Advertentie';
|
||||
|
||||
@@ -1084,6 +1092,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Openbaar';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privé';
|
||||
|
||||
@@ -1444,6 +1457,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Succesvol';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Pad verwijderen';
|
||||
|
||||
@@ -1545,6 +1561,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Nieuw: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Link openen?';
|
||||
|
||||
@@ -1626,6 +1648,24 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Vlaggen';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Deel marker hier';
|
||||
|
||||
@@ -1711,6 +1751,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Gedeelde pin';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Gedeeld';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Kamer Toetreden';
|
||||
|
||||
@@ -3649,6 +3692,37 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrein';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Geplande plaatsen';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Tik op een geblokkeerd gebied om het op de kaart te markeren.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Geselecteerde obstakel';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequentie';
|
||||
|
||||
@@ -4113,4 +4187,40 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Taal van het systeem';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Zamknij';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Edytuj';
|
||||
|
||||
@@ -439,9 +442,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
'Uwzględnij lokalizację w ogłoszeniu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Wielokrotne ACK: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
@@ -450,6 +451,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Działania';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Wyślij rozgłoszenie';
|
||||
|
||||
@@ -1104,6 +1112,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Publiczny';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Prywatny';
|
||||
|
||||
@@ -1468,6 +1481,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Sukcesy';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Usuń ścieżkę';
|
||||
|
||||
@@ -1569,6 +1585,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Nieprzeczytane: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Otworzyć link?';
|
||||
|
||||
@@ -1650,6 +1672,24 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flagi';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
|
||||
|
||||
@@ -1735,6 +1775,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Udostępniona pinezka';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Udostępnione';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Dołącz do pokoju';
|
||||
|
||||
@@ -3677,6 +3720,37 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Teren';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Zablokowane miejsca';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Wybór przeszkody';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Częstotliwość';
|
||||
|
||||
@@ -4147,4 +4221,40 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Język systemu';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Fechar';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Editar';
|
||||
|
||||
@@ -436,9 +439,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
'Incluir localização no anúncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Múltiplas respostas de confirmação: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
|
||||
@@ -446,6 +447,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Ações';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Enviar Publicidade';
|
||||
|
||||
@@ -1095,6 +1103,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Público';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privado';
|
||||
|
||||
@@ -1455,6 +1468,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Sucessos';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Remover caminho';
|
||||
|
||||
@@ -1556,6 +1572,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Não lido: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Abrir link?';
|
||||
|
||||
@@ -1638,6 +1660,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Bandeiras';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
|
||||
|
||||
@@ -1723,6 +1763,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Pin compartilhado';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Compartilhado';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Junte-se à Sala';
|
||||
|
||||
@@ -3662,6 +3705,37 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Locais ocupados';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Toque em um ponto bloqueado para destacá-lo no mapa.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Obstrução selecionada';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequência';
|
||||
|
||||
@@ -4126,4 +4200,40 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Idioma do sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get common_cancel => 'Отмена';
|
||||
|
||||
@override
|
||||
String get common_ok => 'Хорошо';
|
||||
String get common_ok => 'OK';
|
||||
|
||||
@override
|
||||
String get common_connect => 'Коннект';
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Закрыть';
|
||||
|
||||
@override
|
||||
String get common_done => 'Готово';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Изменить';
|
||||
|
||||
@@ -109,7 +112,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_title => 'MeshCore: Открытый исходный код';
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
@@ -436,9 +439,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Включить местоположение в объявление';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мульти-ACK: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Несколько подтверждений';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
|
||||
@@ -446,6 +447,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Отправить анонсирование';
|
||||
|
||||
@@ -504,12 +512,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String settings_aboutVersion(String version) {
|
||||
return 'MeshCore Open, версия $version';
|
||||
return 'MeshCore Open v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_aboutLegalese =>
|
||||
'Проект MeshCore с открытым исходным кодом, 2026 год';
|
||||
String get settings_aboutLegalese => '2026 MeshCore Open Source Project';
|
||||
|
||||
@override
|
||||
String get settings_aboutDescription =>
|
||||
@@ -523,7 +530,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get settings_infoName => 'Имя';
|
||||
|
||||
@override
|
||||
String get settings_infoId => 'Идентификационный номер';
|
||||
String get settings_infoId => 'ID';
|
||||
|
||||
@override
|
||||
String get settings_infoStatus => 'Статус';
|
||||
@@ -654,7 +661,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get appSettings_languageRu => 'Русский';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Украинский';
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
@@ -1096,6 +1103,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Публичный';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'через $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Приватный';
|
||||
|
||||
@@ -1148,7 +1160,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get channels_standardPublicPsk => 'Стандартный публичный PSK';
|
||||
|
||||
@override
|
||||
String get channels_pskHex => 'PSK (шестнадцатеричный код)';
|
||||
String get channels_pskHex => 'PSK (Hex)';
|
||||
|
||||
@override
|
||||
String get channels_generateRandomPsk => 'Сгенерировать случайный PSK';
|
||||
@@ -1401,7 +1413,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugFrame_textTypeCli => 'Интерфейс командной строки';
|
||||
String get debugFrame_textTypeCli => 'CLI';
|
||||
|
||||
@override
|
||||
String get debugFrame_textTypePlain => 'Обычный';
|
||||
@@ -1459,6 +1471,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'успешно';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Оценка';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Удалить маршрут';
|
||||
|
||||
@@ -1561,6 +1576,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Непрочитанных: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Открыть ссылку?';
|
||||
|
||||
@@ -1642,6 +1663,24 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Флаги';
|
||||
|
||||
@override
|
||||
String get map_type => 'Тип';
|
||||
|
||||
@override
|
||||
String get map_path => 'Путь';
|
||||
|
||||
@override
|
||||
String get map_location => 'Местоположение';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Прибл. местоположение';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Публичный ключ';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'напр. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
||||
|
||||
@@ -1727,6 +1766,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Общая метка';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Поделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присоединиться к комнате';
|
||||
|
||||
@@ -2047,7 +2089,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Просмотр телеметрии датчиков и системной статистики';
|
||||
|
||||
@override
|
||||
String get repeater_cli => 'Интерфейс командной строки';
|
||||
String get repeater_cli => 'CLI';
|
||||
|
||||
@override
|
||||
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
||||
@@ -3670,6 +3712,38 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Рельеф';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Зарезервированные места';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Щелкните по заблокированной области, чтобы выделить ее на карте.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle =>
|
||||
'Выбранный объект, препятствующий движению';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Частота';
|
||||
|
||||
@@ -4143,4 +4217,40 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Язык системы';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore работает';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Поддерживает BLE-соединение';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Удалено $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Не удалось удалить: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Не удалось обновить канал: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Чат';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Ретранслятор';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Комната';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Датчик';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Неизвестно';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Zavrieť';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Upraviť';
|
||||
|
||||
@@ -430,9 +433,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Viaceré ACK: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Viaceré ACK';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
@@ -441,6 +442,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Možné akcie';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Odoslať reklamu';
|
||||
|
||||
@@ -1084,6 +1092,11 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Veľké verejné';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Osobné';
|
||||
|
||||
@@ -1445,6 +1458,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Úspechy';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Odstrániť cestu';
|
||||
|
||||
@@ -1546,6 +1562,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Nezriadené: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Otvoriť odkaz?';
|
||||
|
||||
@@ -1627,6 +1649,24 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Zástavy';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Zdieľte značku tu';
|
||||
|
||||
@@ -1712,6 +1752,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Zdieľaný PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Zdieľané';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pripojiť miestnosť';
|
||||
|
||||
@@ -3644,6 +3687,37 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terén';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Zablokované miesta';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Vybraná prekážka';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvencia';
|
||||
|
||||
@@ -4108,4 +4182,40 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Jazyk systému';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Zapri';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Uredi';
|
||||
|
||||
@@ -430,9 +433,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Večkratni potrditvi: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Več potrdil';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
|
||||
@@ -440,6 +441,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Akcije';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Pošlji Oglas';
|
||||
|
||||
@@ -1082,6 +1090,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Javni';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Zasebni';
|
||||
|
||||
@@ -1442,6 +1455,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Uspešni';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Izbriši pot';
|
||||
|
||||
@@ -1541,6 +1557,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Nerešeno: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Odpreti povezavo?';
|
||||
|
||||
@@ -1623,6 +1645,24 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Zapestnice';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Delite točke tukaj.';
|
||||
|
||||
@@ -1707,6 +1747,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Deljeno naslovno geslo';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Deljeno';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pridružiti sobo';
|
||||
|
||||
@@ -3640,6 +3683,37 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Teren';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Zasedena parkirišča';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Dotaknite blokirano točko, da jo označite na zemljeplati.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Izbrano ovire';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvenca';
|
||||
|
||||
@@ -4107,4 +4181,40 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Jezik sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Stänga';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Redigera';
|
||||
|
||||
@@ -428,9 +431,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Flera ACK-meddelanden: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Flera bekräftelser';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
|
||||
@@ -438,6 +439,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Åtgärder';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Skicka Annons';
|
||||
|
||||
@@ -1075,6 +1083,11 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Offentligt';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privat';
|
||||
|
||||
@@ -1437,6 +1450,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'framgångar';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Ta bort sökväg';
|
||||
|
||||
@@ -1535,6 +1551,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Olästa: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Öppna länk?';
|
||||
|
||||
@@ -1616,6 +1638,24 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flaggor';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Dela markeringen här';
|
||||
|
||||
@@ -1701,6 +1741,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Delad PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Delad';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Gå med i rum';
|
||||
|
||||
@@ -3618,6 +3661,37 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terräng';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Reserverade platser';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Klicka på en markerad plats för att framhäva den på kartan.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Vald hinder';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvens';
|
||||
|
||||
@@ -4081,4 +4155,40 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Språk för systemet';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
+299
-191
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => '关闭';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => '编辑';
|
||||
|
||||
@@ -408,9 +411,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => '在广告中包含位置';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '多重ACK:$value';
|
||||
}
|
||||
String get settings_multiAck => '多重ACK';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => '遥测模式已更新';
|
||||
@@ -418,6 +419,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => '操作';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => '发送广播';
|
||||
|
||||
@@ -1023,6 +1031,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => '公共';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => '私有';
|
||||
|
||||
@@ -1370,6 +1383,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => '成功';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => '移除路径';
|
||||
|
||||
@@ -1457,6 +1473,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '未读:$count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => '打开链接?';
|
||||
|
||||
@@ -1535,6 +1557,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => '标志';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => '在此分享标记';
|
||||
|
||||
@@ -1618,6 +1658,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => '共享标记';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '已分享';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '加入房间';
|
||||
|
||||
@@ -3375,6 +3418,36 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => '地形';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => '被占用区域';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint => '点击地图上的某个被遮盖的区域,以突出显示该区域。';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => '选择性阻碍';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => '频率';
|
||||
|
||||
@@ -3783,4 +3856,40 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => '系统语言';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
+85
-8
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Privacy modus is ingeschakeld",
|
||||
"settings_privacyModeDisabled": "Privacy modus is uitgeschakeld",
|
||||
"settings_actions": "Acties",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Verzend Advertentie",
|
||||
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
|
||||
"settings_advertisementSent": "Advertentie verzonden",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"contact_lastSeen": "Laatst gezien",
|
||||
"contact_clearChat": "Chat leegmaken",
|
||||
"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_initialRouteWeight": "เริ่มต้น gewicht van de route",
|
||||
"appSettings_maxRouteWeight": "Maximale gewicht voor de route",
|
||||
@@ -2217,5 +2212,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Geeft een lijst van regio's die het verkeer tijdens overstromingen verbieden.",
|
||||
"repeater_cliHelpStatsPackets": "(Alleen voor seriële verbindingen) Toont statistieken op pakketniveau.",
|
||||
"repeater_cliHelpStatsRadio": "(Alleen voor serienummers) Toont radio-statistieken.",
|
||||
"repeater_cliHelpStatsCore": "(Alleen voor seriële communicatie) Toont de belangrijkste firmware-statistieken."
|
||||
"repeater_cliHelpStatsCore": "(Alleen voor seriële communicatie) Toont de belangrijkste firmware-statistieken.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Meerdere bevestigingen",
|
||||
"map_sharedAt": "Gedeeld",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Geselecteerde obstakel",
|
||||
"losBlockedSpotsHint": "Tik op een geblokkeerd gebied om het op de kaart te markeren.",
|
||||
"losBlockedSpotsTitle": "Geplande plaatsen",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Tryb prywatności włączony",
|
||||
"settings_privacyModeDisabled": "Tryb prywatności wyłączony",
|
||||
"settings_actions": "Działania",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Wyślij rozgłoszenie",
|
||||
"settings_sendAdvertisementSubtitle": "Nadaj obecność teraz",
|
||||
"settings_advertisementSent": "Rozgłoszenie wysłane",
|
||||
@@ -2033,13 +2035,6 @@
|
||||
"contact_settings": "Ustawienia kontaktowe",
|
||||
"contact_lastSeen": "Ostatnio widziany",
|
||||
"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_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu",
|
||||
"appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek",
|
||||
@@ -2052,7 +2047,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
|
||||
"settings_multiAck": "Wielokrotne ACK: {value}",
|
||||
"map_showOverlaps": "Nakładające się klucze przekaźników",
|
||||
"map_runTraceWithReturnPath": "Wróć tą samą ścieżką",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2255,5 +2249,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Wymienia regiony, w których ruch związany z powodziami jest ograniczony.",
|
||||
"repeater_cliHelpStatsPackets": "(Tylko dla serialu) Prezentuje statystyki na poziomie pakietów.",
|
||||
"repeater_cliHelpStatsRadio": "(Tylko serial) Prezentuje statystyki dotyczące nadawania radiowego.",
|
||||
"repeater_cliHelpStatsCore": "(Tylko wersja serialowa) Wyświetla podstawowe statystyki o oprogramowaniu."
|
||||
"repeater_cliHelpStatsCore": "(Tylko wersja serialowa) Wyświetla podstawowe statystyki o oprogramowaniu.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Wielokrotne potwierdzenia odbioru",
|
||||
"map_sharedAt": "Udostępnione",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Wybór przeszkody",
|
||||
"losBlockedSpotsTitle": "Zablokowane miejsca",
|
||||
"losBlockedSpotsHint": "Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-8
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Modo de privacidade ativado",
|
||||
"settings_privacyModeDisabled": "Modo de privacidade desativado",
|
||||
"settings_actions": "Ações",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Enviar Publicidade",
|
||||
"settings_sendAdvertisementSubtitle": "Presença de transmissão agora",
|
||||
"settings_advertisementSent": "Anúncio enviado",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_settings": "Configurações de Contato",
|
||||
"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_maxRouteWeight": "Peso Máximo da Rota",
|
||||
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
|
||||
@@ -2015,6 +2010,7 @@
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
|
||||
"settings_multiAck": "Múltiplas respostas de confirmação: {value}",
|
||||
"settings_multiAck": "Multi-ACKs",
|
||||
"map_showOverlaps": "Sobreposições da Chave Repeater",
|
||||
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2217,5 +2213,86 @@
|
||||
"repeater_cliHelpRegionListDenied": "Lista as regiões que restringem o tráfego em áreas de risco de inundações.",
|
||||
"repeater_cliHelpStatsPackets": "(Apenas para séries) Apresenta estatísticas em nível de pacotes.",
|
||||
"repeater_cliHelpStatsRadio": "(Apenas para transmissões em série) Exibe estatísticas de rádio.",
|
||||
"repeater_cliHelpStatsCore": "(Apenas para dispositivos em série) Exibe estatísticas básicas do firmware."
|
||||
"repeater_cliHelpStatsCore": "(Apenas para dispositivos em série) Exibe estatísticas básicas do firmware.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "Compartilhado",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Locais ocupados",
|
||||
"losBlockedSpotsHint": "Toque em um ponto bloqueado para destacá-lo no mapa.",
|
||||
"losSelectedObstructionTitle": "Obstrução selecionada",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+177
-92
@@ -13,7 +13,7 @@
|
||||
"nav_channels": "Каналы",
|
||||
"nav_map": "Карта",
|
||||
"common_cancel": "Отмена",
|
||||
"common_ok": "Хорошо",
|
||||
"common_ok": "OK",
|
||||
"common_connect": "Коннект",
|
||||
"common_unknownDevice": "Неизвестное устройство",
|
||||
"common_save": "Сохранить",
|
||||
@@ -39,7 +39,7 @@
|
||||
"common_notAvailable": "—",
|
||||
"common_voltageValue": "{volts} В",
|
||||
"common_percentValue": "{percent}%",
|
||||
"scanner_title": "MeshCore: Открытый исходный код",
|
||||
"scanner_title": "MeshCore Open",
|
||||
"scanner_scanning": "Поиск устройств...",
|
||||
"scanner_connecting": "Подключение...",
|
||||
"scanner_disconnecting": "Отключение...",
|
||||
@@ -81,6 +81,8 @@
|
||||
"settings_privacyModeEnabled": "Режим конфиденциальности включен",
|
||||
"settings_privacyModeDisabled": "Режим конфиденциальности выключен",
|
||||
"settings_actions": "Действия",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Отправить анонсирование",
|
||||
"settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас",
|
||||
"settings_advertisementSent": "Анонсирование отправлено",
|
||||
@@ -98,11 +100,11 @@
|
||||
"settings_appDebugLog": "Журнал отладки приложения",
|
||||
"settings_appDebugLogSubtitle": "Сообщения отладки приложения",
|
||||
"settings_about": "О программе",
|
||||
"settings_aboutVersion": "MeshCore Open, версия {version}",
|
||||
"settings_aboutLegalese": "Проект MeshCore с открытым исходным кодом, 2026 год",
|
||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||
"settings_aboutLegalese": "2026 MeshCore Open Source Project",
|
||||
"settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.",
|
||||
"settings_infoName": "Имя",
|
||||
"settings_infoId": "Идентификационный номер",
|
||||
"settings_infoId": "ID",
|
||||
"settings_infoStatus": "Статус",
|
||||
"settings_infoBattery": "Батарея",
|
||||
"settings_infoPublicKey": "Публичный ключ",
|
||||
@@ -245,7 +247,7 @@
|
||||
"channels_channelName": "Имя канала",
|
||||
"channels_usePublicChannel": "Использовать публичный канал",
|
||||
"channels_standardPublicPsk": "Стандартный публичный PSK",
|
||||
"channels_pskHex": "PSK (шестнадцатеричный код)",
|
||||
"channels_pskHex": "PSK (Hex)",
|
||||
"channels_generateRandomPsk": "Сгенерировать случайный PSK",
|
||||
"channels_enterChannelName": "Введите имя канала",
|
||||
"channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа",
|
||||
@@ -317,7 +319,7 @@
|
||||
"debugFrame_timestamp": "- Временная метка: {timestamp}",
|
||||
"debugFrame_flags": "- Флаги: 0x{value}",
|
||||
"debugFrame_textType": "- Тип текста: {type} ({label})",
|
||||
"debugFrame_textTypeCli": "Интерфейс командной строки",
|
||||
"debugFrame_textTypeCli": "CLI",
|
||||
"debugFrame_textTypePlain": "Обычный",
|
||||
"debugFrame_text": "- Текст: \"{text}\"",
|
||||
"debugFrame_hexDump": "Шестнадцатеричный дамп:",
|
||||
@@ -476,7 +478,7 @@
|
||||
"repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера",
|
||||
"repeater_telemetry": "Телеметрия",
|
||||
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
||||
"repeater_cli": "Интерфейс командной строки",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cliSubtitle": "Отправка команд репитеру",
|
||||
"repeater_neighbors": "Соседи",
|
||||
"repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.",
|
||||
@@ -560,88 +562,15 @@
|
||||
"repeater_errorSendingCommand": "Ошибка отправки команды: {error}",
|
||||
"repeater_confirm": "Подтвердить",
|
||||
"repeater_settingsSaved": "Настройки успешно сохранены",
|
||||
"repeater_rxGain": "Увеличенная эффективность RX",
|
||||
"repeater_rxGainHelper": "Более высокая чувствительность, больший ток потребления (только для SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "Обновите усиление RX",
|
||||
"repeater_multiAcks": "Несколько подтверждений",
|
||||
"repeater_multiAcksSubtitle": "Обеспечьте доставку сообщений по нескольким каналам для повышения эффективности.",
|
||||
"repeater_refreshMultiAcks": "Обновление нескольких подтверждений",
|
||||
"repeater_networkHealth": "Состояние сети",
|
||||
"repeater_loopDetect": "Обнаружение циклов",
|
||||
"repeater_loopDetectHelper": "Создайте пакеты данных, которые выглядят как циклы маршрутизации.",
|
||||
"repeater_loopDetectOff": "Отключено",
|
||||
"repeater_loopDetectMinimal": "Минимальный",
|
||||
"repeater_loopDetectModerate": "Умеренный",
|
||||
"repeater_loopDetectStrict": "Строгий",
|
||||
"repeater_dutyCycle": "Цикл работы",
|
||||
"repeater_dutyCycleHelper": "Максимальный процент времени, выделенного на трансляцию.",
|
||||
"repeater_dutyCyclePercent": "{percent}%",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "Информация о операторе",
|
||||
"repeater_ownerInfoHelper": "Общая метаинформация для этого ретранслятора",
|
||||
"repeater_refreshOwnerInfo": "Обновить информацию о операторе",
|
||||
"repeater_floodMax": "Максимальное количество прыжков при наводнении",
|
||||
"repeater_floodMaxHelper": "Максимальное количество пакетов, которые могут быть отправлены в одном потоке (0-64)",
|
||||
"repeater_advancedSettings": "Продвинутый",
|
||||
"repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов",
|
||||
"repeater_pathHashMode": "Режим хеширования пути",
|
||||
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.",
|
||||
"repeater_txDelay": "Задержка в работе системы Flood TX",
|
||||
"repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.",
|
||||
"repeater_directTxDelay": "Прямая задержка сигнала TX",
|
||||
"repeater_directTxDelayHelper": "Передача промежуточных данных для прямого (немассового) трафика, в качестве коэффициента, равного времени передачи пакета (от 0 до 2, по умолчанию 0,3).",
|
||||
"repeater_intThresh": "Пороговое значение помех",
|
||||
"repeater_intThreshHelper": "Порог устанавливается для калибровки уровня шума радио, чтобы оно отсеивало помехи, превышающие этот уровень. Значение \"0\" означает отключение – используйте только в случае, если вы наблюдаете ошибки при приеме сигнала в шумном диапазоне.",
|
||||
"repeater_agcResetInterval": "Интервал сброса AGC",
|
||||
"repeater_agcResetIntervalHelper": "Как часто следует сбрасывать автоматическую регулировку усиления радио, чтобы вернуться к нормальному состоянию после заклинивания? Интервал сброса составляет несколько секунд, кратный 4. Отключение периодического сброса осуществляется с помощью параметра 0.",
|
||||
"repeater_actionsTitle": "Действия",
|
||||
"repeater_sendAdvert": "Отправить объявление о наводнении",
|
||||
"repeater_sendAdvertSubtitle": "Разместите рекламу о наводнении в эфире по всей сети.",
|
||||
"repeater_sendAdvertZeroHop": "Опубликуйте рекламу, не требующую промежуточного распространения.",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "Разместите рекламу, распространяемую одним способом (без использования ретрансляторов).",
|
||||
"repeater_clockSync": "Синхронизировать время сейчас",
|
||||
"repeater_clockSyncSubtitle": "Установите время на вашем телефоне, чтобы оно совпадало со временем ретранслятора.",
|
||||
"repeater_actionSucceeded": "{action} succeeded",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} failed: {error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "Настройки сохранены — перезагрузите ретранслятор, чтобы применить их.",
|
||||
"repeater_settingsPartialFailure": "Некоторые настройки не удалось применить: {failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}",
|
||||
"repeater_refreshBasicSettings": "Обновить основные настройки",
|
||||
"repeater_refreshRadioSettings": "Обновить настройки радио",
|
||||
"repeater_refreshTxPower": "Обновить мощность передачи",
|
||||
"repeater_refreshLocationSettings": "Обновить настройки местоположения",
|
||||
"repeater_refreshPacketForwarding": "Обновить пересылку пакетов",
|
||||
"repeater_refreshGuestAccess": "Обновить гостевой доступ",
|
||||
"repeater_refreshPrivacyMode": "Обновить режим конфиденциальности",
|
||||
"repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований",
|
||||
"repeater_refreshed": "{label} обновлён",
|
||||
"repeater_errorRefreshing": "Ошибка обновления {label}",
|
||||
"repeater_cliTitle": "CLI репитера",
|
||||
@@ -877,7 +806,7 @@
|
||||
"contacts_contactImportFailed": "Контакт не удалось импортировать",
|
||||
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
|
||||
"contacts_zeroHopAdvert": "Реклама Zero Hop",
|
||||
"appSettings_languageUk": "Украинский",
|
||||
"appSettings_languageUk": "Українська",
|
||||
"appSettings_enableMessageTracing": "Включить трассировку сообщений",
|
||||
"appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений",
|
||||
"contacts_floodAdvert": "Рекламный поток",
|
||||
@@ -1235,13 +1164,6 @@
|
||||
"contact_clearChat": "Очистить чат",
|
||||
"contact_lastSeen": "Последний раз видели",
|
||||
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
|
||||
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
|
||||
@@ -1254,7 +1176,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
|
||||
"settings_multiAck": "Мульти-ACK: {value}",
|
||||
"map_showOverlaps": "Перекрытия ключа повтора",
|
||||
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -1387,6 +1308,170 @@
|
||||
"repeater_guest": "Информация о ретрансляторе",
|
||||
"room_guest": "Информация о сервере",
|
||||
"repeater_guestTools": "Инструменты для гостей",
|
||||
"common_done": "Готово",
|
||||
"background_serviceTitle": "MeshCore работает",
|
||||
"background_serviceText": "Поддерживает BLE-соединение",
|
||||
"appSettings_translationModelDeleted": "Удалено {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Не удалось удалить: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Не удалось обновить канал: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Тип",
|
||||
"map_path": "Путь",
|
||||
"map_location": "Местоположение",
|
||||
"map_estLocation": "Прибл. местоположение",
|
||||
"map_publicKey": "Публичный ключ",
|
||||
"map_publicKeyPrefixHint": "напр. ab12",
|
||||
"contact_typeChat": "Чат",
|
||||
"contact_typeRepeater": "Ретранслятор",
|
||||
"contact_typeRoom": "Комната",
|
||||
"contact_typeSensor": "Датчик",
|
||||
"contact_typeUnknown": "Неизвестно",
|
||||
"channels_via": "через {path}",
|
||||
"chat_score": "Оценка",
|
||||
"settings_multiAck": "Несколько подтверждений",
|
||||
"map_sharedAt": "Поделено",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Щелкните по заблокированной области, чтобы выделить ее на карте.",
|
||||
"losBlockedSpotsTitle": "Зарезервированные места",
|
||||
"losSelectedObstructionTitle": "Выбранный объект, препятствующий движению",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"repeater_rxGain": "Увеличенная эффективность RX",
|
||||
"repeater_rxGainHelper": "Более высокая чувствительность, больший ток потребления (только для SX1262/SX1268)",
|
||||
"repeater_refreshRxGain": "Обновите усиление RX",
|
||||
"repeater_multiAcks": "Несколько подтверждений",
|
||||
"repeater_multiAcksSubtitle": "Обеспечьте доставку сообщений по нескольким каналам для повышения эффективности.",
|
||||
"repeater_refreshMultiAcks": "Обновление нескольких подтверждений",
|
||||
"repeater_networkHealth": "Состояние сети",
|
||||
"repeater_loopDetect": "Обнаружение циклов",
|
||||
"repeater_loopDetectHelper": "Создайте пакеты данных, которые выглядят как циклы маршрутизации.",
|
||||
"repeater_loopDetectOff": "Отключено",
|
||||
"repeater_loopDetectMinimal": "Минимальный",
|
||||
"repeater_loopDetectModerate": "Умеренный",
|
||||
"repeater_loopDetectStrict": "Строгий",
|
||||
"repeater_dutyCycle": "Цикл работы",
|
||||
"repeater_dutyCycleHelper": "Максимальный процент времени, выделенного на трансляцию.",
|
||||
"repeater_dutyCyclePercent": "{percent}%",
|
||||
"@repeater_dutyCyclePercent": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_ownerInfo": "Информация о операторе",
|
||||
"repeater_ownerInfoHelper": "Общая метаинформация для этого ретранслятора",
|
||||
"repeater_refreshOwnerInfo": "Обновить информацию о операторе",
|
||||
"repeater_floodMax": "Максимальное количество прыжков при наводнении",
|
||||
"repeater_floodMaxHelper": "Максимальное количество пакетов, которые могут быть отправлены в одном потоке (0-64)",
|
||||
"repeater_advancedSettings": "Продвинутый",
|
||||
"repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов",
|
||||
"repeater_pathHashMode": "Режим хеширования пути",
|
||||
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.",
|
||||
"repeater_txDelay": "Задержка в работе системы Flood TX",
|
||||
"repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.",
|
||||
"repeater_directTxDelay": "Прямая задержка сигнала TX",
|
||||
"repeater_directTxDelayHelper": "Передача промежуточных данных для прямого (немассового) трафика, в качестве коэффициента, равного времени передачи пакета (от 0 до 2, по умолчанию 0,3).",
|
||||
"repeater_intThresh": "Пороговое значение помех",
|
||||
"repeater_intThreshHelper": "Порог устанавливается для калибровки уровня шума радио, чтобы оно отсеивало помехи, превышающие этот уровень. Значение \"0\" означает отключение – используйте только в случае, если вы наблюдаете ошибки при приеме сигнала в шумном диапазоне.",
|
||||
"repeater_agcResetInterval": "Интервал сброса AGC",
|
||||
"repeater_agcResetIntervalHelper": "Как часто следует сбрасывать автоматическую регулировку усиления радио, чтобы вернуться к нормальному состоянию после заклинивания? Интервал сброса составляет несколько секунд, кратный 4. Отключение периодического сброса осуществляется с помощью параметра 0.",
|
||||
"repeater_actionsTitle": "Действия",
|
||||
"repeater_sendAdvert": "Отправить объявление о наводнении",
|
||||
"repeater_sendAdvertSubtitle": "Разместите рекламу о наводнении в эфире по всей сети.",
|
||||
"repeater_sendAdvertZeroHop": "Опубликуйте рекламу, не требующую промежуточного распространения.",
|
||||
"repeater_sendAdvertZeroHopSubtitle": "Разместите рекламу, распространяемую одним способом (без использования ретрансляторов).",
|
||||
"repeater_clockSync": "Синхронизировать время сейчас",
|
||||
"repeater_clockSyncSubtitle": "Установите время на вашем телефоне, чтобы оно совпадало со временем ретранслятора.",
|
||||
"repeater_actionSucceeded": "{action} succeeded",
|
||||
"@repeater_actionSucceeded": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_actionFailed": "{action} failed: {error}",
|
||||
"@repeater_actionFailed": {
|
||||
"placeholders": {
|
||||
"action": {
|
||||
"type": "String"
|
||||
},
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsSavedRebootNeeded": "Настройки сохранены — перезагрузите ретранслятор, чтобы применить их.",
|
||||
"repeater_settingsPartialFailure": "Некоторые настройки не удалось применить: {failures}",
|
||||
"@repeater_settingsPartialFailure": {
|
||||
"placeholders": {
|
||||
"failures": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@common_percentValue": {
|
||||
"placeholders": {
|
||||
"percent": {
|
||||
|
||||
+85
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Ochranný režim je povolený.",
|
||||
"settings_privacyModeDisabled": "Ochranný režim je vypnutý",
|
||||
"settings_actions": "Možné akcie",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Odoslať reklamu",
|
||||
"settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.",
|
||||
"settings_advertisementSent": "Reklama odeslaná",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"contact_lastSeen": "Naposledy videný",
|
||||
"contact_teleBase": "Báza telemetrie",
|
||||
"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_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
|
||||
"appSettings_initialRouteWeight": "Počiatočná váha trasy",
|
||||
@@ -2014,7 +2009,7 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2217,5 +2212,86 @@
|
||||
"repeater_cliHelpRegionListDenied": "Zoznam oblastí, ktoré zakazujú premávku v dôsledku povodní.",
|
||||
"repeater_cliHelpStatsPackets": "(Len pre sériové záznamy) Zobrazuje štatistiky na úrovni paketov.",
|
||||
"repeater_cliHelpStatsRadio": "(Len pre sériu) Zobrazuje údaje o rádiových staniciach.",
|
||||
"repeater_cliHelpStatsCore": "(Len pre sériové modely) Zobrazuje základné štatistiky firmvéru."
|
||||
"repeater_cliHelpStatsCore": "(Len pre sériové modely) Zobrazuje základné štatistiky firmvéru.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "Zdieľané",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Zablokované miesta",
|
||||
"losBlockedSpotsHint": "Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.",
|
||||
"losSelectedObstructionTitle": "Vybraná prekážka",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Privatni način je omogočen.",
|
||||
"settings_privacyModeDisabled": "Privatni način je onemogočen.",
|
||||
"settings_actions": "Akcije",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Pošlji Oglas",
|
||||
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
|
||||
"settings_advertisementSent": "Oglas poslan",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"contact_teleEnv": "Okolje telemetrije",
|
||||
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
|
||||
"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_initialRouteWeight": "Izvirna teža poti",
|
||||
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
|
||||
@@ -2013,7 +2008,6 @@
|
||||
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Večkratni potrditvi: {value}",
|
||||
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
|
||||
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
|
||||
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.",
|
||||
@@ -2217,5 +2211,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Navaja regije, ki preprečujejo promet zaradi poplav.",
|
||||
"repeater_cliHelpStatsPackets": "(Samo za serijske povezave) Prikazuje statistiko na nivoju paketov.",
|
||||
"repeater_cliHelpStatsRadio": "(Samo za serije) Prikazuje statistične podatke o radiju.",
|
||||
"repeater_cliHelpStatsCore": "(Samo za serijske naprave) Prikazuje osnovne statistične podatke."
|
||||
"repeater_cliHelpStatsCore": "(Samo za serijske naprave) Prikazuje osnovne statistične podatke.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Več potrdil",
|
||||
"map_sharedAt": "Deljeno",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Dotaknite blokirano točko, da jo označite na zemljeplati.",
|
||||
"losSelectedObstructionTitle": "Izbrano ovire",
|
||||
"losBlockedSpotsTitle": "Zasedena parkirišča",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-8
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Privatläget är aktiverat",
|
||||
"settings_privacyModeDisabled": "Privatläge är avstängt",
|
||||
"settings_actions": "Åtgärder",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Skicka Annons",
|
||||
"settings_sendAdvertisementSubtitle": "Sändning finns nu",
|
||||
"settings_advertisementSent": "Annons skickad",
|
||||
@@ -1995,13 +1997,6 @@
|
||||
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
|
||||
"contact_teleLoc": "Telemetridata plats",
|
||||
"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_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
|
||||
@@ -2217,5 +2212,87 @@
|
||||
"repeater_cliHelpRegionListDenied": "Listar områden där trafik på grund av översvämningar är förbjuden.",
|
||||
"repeater_cliHelpStatsPackets": "(Endast för seriell kommunikation) Visar statistik på paketnivå.",
|
||||
"repeater_cliHelpStatsRadio": "(Enbart för serier) Visar radiostatistik.",
|
||||
"repeater_cliHelpStatsCore": "(Enbart för seriell kommunikation) Visar grundläggande firmware-statistik."
|
||||
"repeater_cliHelpStatsCore": "(Enbart för seriell kommunikation) Visar grundläggande firmware-statistik.",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Flera bekräftelser",
|
||||
"map_sharedAt": "Delad",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsTitle": "Reserverade platser",
|
||||
"losSelectedObstructionTitle": "Vald hinder",
|
||||
"losBlockedSpotsHint": "Klicka på en markerad plats för att framhäva den på kartan.",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+241
-185
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
|
||||
"channels_channelDeleteFailed": "Не вдалось видалити канал \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -18,6 +18,7 @@
|
||||
"common_save": "Зберегти",
|
||||
"common_delete": "Видалити",
|
||||
"common_close": "Закрити",
|
||||
"common_done": "Готово",
|
||||
"common_edit": "Редагувати",
|
||||
"common_add": "Додати",
|
||||
"common_settings": "Налаштування",
|
||||
@@ -26,7 +27,7 @@
|
||||
"common_disconnected": "Відключено",
|
||||
"common_create": "Створити",
|
||||
"common_continue": "Продовжити",
|
||||
"common_share": "Поділитися",
|
||||
"common_share": "Поділитись",
|
||||
"common_copy": "Копіювати",
|
||||
"common_retry": "Повторити",
|
||||
"common_hide": "Приховати",
|
||||
@@ -81,7 +82,7 @@
|
||||
"device_meshcore": "MeshCore",
|
||||
"settings_title": "Налаштування",
|
||||
"settings_deviceInfo": "Інформація про пристрій",
|
||||
"settings_appSettings": "Налаштування програми",
|
||||
"settings_appSettings": "Налаштування застосунку",
|
||||
"settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти",
|
||||
"settings_nodeSettings": "Налаштування вузла",
|
||||
"settings_nodeName": "Ім'я вузла",
|
||||
@@ -91,19 +92,21 @@
|
||||
"settings_radioSettings": "Налаштування радіо",
|
||||
"settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення",
|
||||
"settings_radioSettingsUpdated": "Налаштування радіо оновлено",
|
||||
"settings_location": "Розташування",
|
||||
"settings_locationSubtitle": "GPS координати",
|
||||
"settings_locationUpdated": "Розташування оновлено",
|
||||
"settings_location": "Геопозиція",
|
||||
"settings_locationSubtitle": "GPS-координати",
|
||||
"settings_locationUpdated": "Геопозицію оновлено",
|
||||
"settings_locationBothRequired": "Введіть широту та довготу.",
|
||||
"settings_locationInvalid": "Некоректна широта або довгота.",
|
||||
"settings_latitude": "Широта",
|
||||
"settings_longitude": "Довгота",
|
||||
"settings_privacyMode": "Режим приватності",
|
||||
"settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях",
|
||||
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.",
|
||||
"settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
|
||||
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
|
||||
"settings_privacyModeEnabled": "Режим приватності увімкнено",
|
||||
"settings_privacyModeDisabled": "Режим приватності вимкнено",
|
||||
"settings_actions": "Дії",
|
||||
"settings_deleteAllPaths": "Видалити всі шляхи",
|
||||
"settings_deleteAllPathsSubtitle": "Очистити всі дані шляхів у контактах.",
|
||||
"settings_sendAdvertisement": "Оголосити себе",
|
||||
"settings_sendAdvertisementSubtitle": "Транслювати присутність зараз",
|
||||
"settings_advertisementSent": "Оголошення надіслано",
|
||||
@@ -118,10 +121,10 @@
|
||||
"settings_debug": "Налагодження",
|
||||
"settings_bleDebugLog": "Журнал налагодження BLE",
|
||||
"settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані",
|
||||
"settings_appDebugLog": "Журнал налагодження програми",
|
||||
"settings_appDebugLogSubtitle": "Повідомлення налагодження програми",
|
||||
"settings_about": "Про програму",
|
||||
"settings_aboutVersion": "MeshCore Open версії {version}",
|
||||
"settings_appDebugLog": "Журнал налагодження застосунку",
|
||||
"settings_appDebugLogSubtitle": "Повідомлення налагодження застосунку",
|
||||
"settings_about": "Про застосунок",
|
||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||
"@settings_aboutVersion": {
|
||||
"placeholders": {
|
||||
"version": {
|
||||
@@ -156,7 +159,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_title": "Налаштування програми",
|
||||
"appSettings_title": "Налаштування застосунку",
|
||||
"appSettings_appearance": "Вигляд",
|
||||
"appSettings_theme": "Тема",
|
||||
"appSettings_themeSystem": "Системна",
|
||||
@@ -194,9 +197,9 @@
|
||||
"appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб",
|
||||
"appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання",
|
||||
"appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.",
|
||||
"appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.",
|
||||
"appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатись автоматично.",
|
||||
"appSettings_autoRouteRotation": "Авторотація маршруту",
|
||||
"appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)",
|
||||
"appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «через всю мережу» (flood)",
|
||||
"appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена",
|
||||
"appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена",
|
||||
"appSettings_battery": "Батарея",
|
||||
@@ -251,10 +254,10 @@
|
||||
}
|
||||
},
|
||||
"appSettings_debugCard": "Налагодження",
|
||||
"appSettings_appDebugLogging": "Логування налагодження програми",
|
||||
"appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.",
|
||||
"appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено",
|
||||
"appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.",
|
||||
"appSettings_appDebugLogging": "Журналювання налагодження застосунку",
|
||||
"appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження застосунку в журнал для усунення несправностей.",
|
||||
"appSettings_appDebugLoggingEnabled": "Журналювання налагодження застосунку увімкнено",
|
||||
"appSettings_appDebugLoggingDisabled": "Журналювання налагодження застосунку вимкнено.",
|
||||
"contacts_title": "Контакти",
|
||||
"contacts_noContacts": "Контактів не знайдено.",
|
||||
"contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.",
|
||||
@@ -338,8 +341,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "Канал з хештегом",
|
||||
"channels_hashtagChannel": "Хештег-канал",
|
||||
"channels_public": "Публічний",
|
||||
"channels_via": "через {path}",
|
||||
"channels_private": "Приватний",
|
||||
"channels_publicChannel": "Публічний канал",
|
||||
"channels_privateChannel": "Приватний канал",
|
||||
@@ -371,7 +375,7 @@
|
||||
"channels_pskHex": "PSK (шестнадцяткова система)",
|
||||
"channels_generateRandomPsk": "Згенерувати випадковий ключ PSK",
|
||||
"channels_enterChannelName": "Будь ласка, введіть назву каналу",
|
||||
"channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.",
|
||||
"channels_pskMustBe32Hex": "PSK має складатись з 32 шістнадцяткових символів.",
|
||||
"channels_channelAdded": "Канал «{name}» додано",
|
||||
"@channels_channelAdded": {
|
||||
"placeholders": {
|
||||
@@ -422,7 +426,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_location": "Розташування",
|
||||
"chat_location": "Геопозиція",
|
||||
"chat_sendMessageTo": "Надіслати повідомлення {contactName}",
|
||||
"@chat_sendMessageTo": {
|
||||
"placeholders": {
|
||||
@@ -466,17 +470,17 @@
|
||||
"gifPicker_searchHint": "Пошук GIF...",
|
||||
"gifPicker_poweredBy": "На базі GIPHY",
|
||||
"gifPicker_noGifsFound": "GIF не знайдено",
|
||||
"gifPicker_failedLoad": "Не вдалося завантажити GIF-файли",
|
||||
"gifPicker_failedLoad": "Не вдалось завантажити GIF-файли",
|
||||
"gifPicker_failedSearch": "Пошук GIF не вдався",
|
||||
"gifPicker_noInternet": "Немає інтернет-з'єднання",
|
||||
"debugLog_appTitle": "Журнал налагодження програми",
|
||||
"debugLog_appTitle": "Журнал налагодження застосунку",
|
||||
"debugLog_bleTitle": "Журнал налагодження BLE",
|
||||
"debugLog_copyLog": "Копіювати журнал",
|
||||
"debugLog_clearLog": "Очистити журнал",
|
||||
"debugLog_copied": "Журнал налагодження скопійовано",
|
||||
"debugLog_bleCopied": "Журнал BLE скопійовано",
|
||||
"debugLog_noEntries": "Поки що немає записів журналу налагодження.",
|
||||
"debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях",
|
||||
"debugLog_enableInSettings": "Увімкніть налагодження застосунку в налаштуваннях",
|
||||
"debugLog_frames": "Кадри",
|
||||
"debugLog_rawLogRx": "Необроблений лог - RX",
|
||||
"debugLog_noBleActivity": "Поки що немає активності BLE.",
|
||||
@@ -546,12 +550,12 @@
|
||||
"chat_pathManagement": "Керування шляхами",
|
||||
"chat_routingMode": "Режим маршрутизації",
|
||||
"chat_autoUseSavedPath": "Авто (використовувати збережений шлях)",
|
||||
"chat_forceFloodMode": "Примусово на всю мережу",
|
||||
"chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):",
|
||||
"chat_forceFloodMode": "Примусово через всю мережу",
|
||||
"chat_recentAckPaths": "Підтверджені шляхи (натисніть, щоб використати):",
|
||||
"chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.",
|
||||
"chat_hopSingular": "Стрибок",
|
||||
"chat_hopPlural": "стрибків",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}",
|
||||
"chat_hopSingular": "Перехід",
|
||||
"chat_hopPlural": "переходів",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{перехід} few{переходи} many{переходів} other{переходів}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -560,6 +564,7 @@
|
||||
}
|
||||
},
|
||||
"chat_successes": "Успішно",
|
||||
"chat_score": "Оцінка",
|
||||
"chat_removePath": "Видалити шлях",
|
||||
"chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.",
|
||||
"chat_pathActions": "Дії зі шляхом:",
|
||||
@@ -568,11 +573,11 @@
|
||||
"chat_clearPath": "Очистити шлях",
|
||||
"chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні",
|
||||
"chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.",
|
||||
"chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми",
|
||||
"chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.",
|
||||
"chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі застосунку",
|
||||
"chat_floodModeEnabled": "Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.",
|
||||
"chat_fullPath": "Повний шлях",
|
||||
"chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.",
|
||||
"chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}",
|
||||
"chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{перехід} few{переходи} many{переходів} other{переходів}} - {status}",
|
||||
"@chat_pathSetHops": {
|
||||
"placeholders": {
|
||||
"hopCount": {
|
||||
@@ -590,9 +595,9 @@
|
||||
"chat_path": "Шлях",
|
||||
"chat_publicKey": "Відкритий ключ",
|
||||
"chat_compressOutgoingMessages": "Стискати вихідні повідомлення",
|
||||
"chat_floodForced": "На всю мережу (примусово)",
|
||||
"chat_directForced": "Прямий (примусово)",
|
||||
"chat_hopsForced": "{count} стрибків (примусово)",
|
||||
"chat_floodForced": "Через всю мережу (примусово)",
|
||||
"chat_directForced": "Напряму (примусово)",
|
||||
"chat_hopsForced": "{count} переходів (примусово)",
|
||||
"@chat_hopsForced": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -600,9 +605,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_floodAuto": "На всю мережу (авто)",
|
||||
"chat_direct": "Прямий",
|
||||
"chat_poiShared": "Точкою інтересу поділилися",
|
||||
"chat_floodAuto": "Через всю мережу (авто)",
|
||||
"chat_direct": "Напряму",
|
||||
"chat_poiShared": "Поділилися точкою інтересу",
|
||||
"chat_unread": "Непрочитано: {count}",
|
||||
"@chat_unread": {
|
||||
"placeholders": {
|
||||
@@ -614,7 +619,7 @@
|
||||
"chat_openLink": "Відкрити посилання?",
|
||||
"chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?",
|
||||
"chat_open": "Відкрити",
|
||||
"chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}",
|
||||
"chat_couldNotOpenLink": "Не вдалось відкрити посилання: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
@@ -624,8 +629,8 @@
|
||||
},
|
||||
"chat_invalidLink": "Невірний формат посилання",
|
||||
"map_title": "Карта вузлів",
|
||||
"map_noNodesWithLocation": "Немає вузлів з даними про розташування",
|
||||
"map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.",
|
||||
"map_noNodesWithLocation": "Немає вузлів з даними про геопозицію",
|
||||
"map_nodesNeedGps": "Вузли мають надавати свої GPS координати,\nщоб з'явитись на карті.",
|
||||
"map_nodesCount": "Вузли: {count}",
|
||||
"@map_nodesCount": {
|
||||
"placeholders": {
|
||||
@@ -650,19 +655,25 @@
|
||||
"map_pinPrivate": "Замок (Приватний)",
|
||||
"map_pinPublic": "Ключ (Публічний)",
|
||||
"map_lastSeen": "Останній раз бачили",
|
||||
"map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?",
|
||||
"map_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?",
|
||||
"map_from": "Від",
|
||||
"map_source": "Джерело",
|
||||
"map_flags": "Прапорці",
|
||||
"map_shareMarkerHere": "Поділитися маркером тут",
|
||||
"map_type": "Тип",
|
||||
"map_path": "Шлях",
|
||||
"map_location": "Геопозиція",
|
||||
"map_estLocation": "Орієнтовна геопозиція",
|
||||
"map_publicKey": "Публічний ключ",
|
||||
"map_publicKeyPrefixHint": "напр. ab12",
|
||||
"map_shareMarkerHere": "Поділитись маркером тут",
|
||||
"map_pinLabel": "Мітка піна",
|
||||
"map_label": "Мітка",
|
||||
"map_pointOfInterest": "Точка інтересу",
|
||||
"map_sendToContact": "Надіслати контакту",
|
||||
"map_sendToChannel": "Надіслати в канал",
|
||||
"map_noChannelsAvailable": "Немає доступних каналів",
|
||||
"map_publicLocationShare": "Поділитися в публічному місці",
|
||||
"map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.",
|
||||
"map_publicLocationShare": "Поділитись в публічному місці",
|
||||
"map_publicLocationShareConfirm": "Ви збираєтесь поділитись геопозицією у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.",
|
||||
"@map_publicLocationShareConfirm": {
|
||||
"placeholders": {
|
||||
"channelLabel": {
|
||||
@@ -670,7 +681,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами",
|
||||
"map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитись маркерами",
|
||||
"map_filterNodes": "Фільтрувати вузли",
|
||||
"map_nodeTypes": "Типи вузлів",
|
||||
"map_chatNodes": "Вузли чату",
|
||||
@@ -683,7 +694,7 @@
|
||||
"map_showSharedMarkers": "Показувати спільні маркери",
|
||||
"map_lastSeenTime": "Час останньої активності",
|
||||
"map_sharedPin": "Спільний пін",
|
||||
"map_joinRoom": "Приєднатися до кімнати",
|
||||
"map_joinRoom": "Приєднатись до кімнати",
|
||||
"map_manageRepeater": "Керувати ретранслятором",
|
||||
"mapCache_title": "Офлайн-кеш карти",
|
||||
"mapCache_selectAreaFirst": "Спершу виберіть область для кешування",
|
||||
@@ -806,7 +817,7 @@
|
||||
"time_minutes": "хвилин",
|
||||
"time_allTime": "Весь час",
|
||||
"dialog_disconnect": "Відключити",
|
||||
"dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?",
|
||||
"dialog_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?",
|
||||
"login_repeaterLogin": "Вхід у ретранслятор",
|
||||
"login_roomLogin": "Вхід у кімнату",
|
||||
"login_password": "Пароль",
|
||||
@@ -817,8 +828,8 @@
|
||||
"login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.",
|
||||
"login_routing": "Маршрутизація",
|
||||
"login_routingMode": "Режим маршрутизації",
|
||||
"login_autoUseSavedPath": "Авто (використовувати збережений шлях)",
|
||||
"login_forceFloodMode": "Примусово на всю мережу",
|
||||
"login_autoUseSavedPath": "Авто (збережений шлях)",
|
||||
"login_forceFloodMode": "Примусово через всю мережу",
|
||||
"login_managePaths": "Керувати шляхами",
|
||||
"login_login": "Вхід",
|
||||
"login_attempt": "Спроба {current}/{max}",
|
||||
@@ -851,7 +862,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}",
|
||||
"path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{переходом} few{переходами} many{переходами} other{переходами}}",
|
||||
"@path_usingHopsPath": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -861,10 +872,10 @@
|
||||
},
|
||||
"path_enterCustomPath": "Ввести власний шлях",
|
||||
"path_currentPathLabel": "Поточний шлях",
|
||||
"path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.",
|
||||
"path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.",
|
||||
"path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).",
|
||||
"path_labelHexPrefixes": "Hex-префікси",
|
||||
"path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)",
|
||||
"path_helperMaxHops": "Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)",
|
||||
"path_selectFromContacts": "Вибрати з контактів:",
|
||||
"path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.",
|
||||
"path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.",
|
||||
@@ -876,7 +887,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.",
|
||||
"path_tooLong": "Шлях занадто довгий. Максимум 64 переходи.",
|
||||
"path_setPath": "Встановити шлях",
|
||||
"repeater_management": "Керування ретранслятором",
|
||||
"repeater_managementTools": "Інструменти керування",
|
||||
@@ -891,7 +902,7 @@
|
||||
"repeater_statusTitle": "Статус ретранслятора",
|
||||
"repeater_routingMode": "Режим маршрутизації",
|
||||
"repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)",
|
||||
"repeater_forceFloodMode": "Примусово на всю мережу",
|
||||
"repeater_forceFloodMode": "Примусово через всю мережу",
|
||||
"repeater_pathManagement": "Керування шляхами",
|
||||
"repeater_refresh": "Оновити",
|
||||
"repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.",
|
||||
@@ -936,7 +947,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}",
|
||||
"repeater_packetTxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}",
|
||||
"@repeater_packetTxTotal": {
|
||||
"placeholders": {
|
||||
"total": {
|
||||
@@ -950,7 +961,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}",
|
||||
"repeater_packetRxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}",
|
||||
"@repeater_packetRxTotal": {
|
||||
"placeholders": {
|
||||
"total": {
|
||||
@@ -964,7 +975,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}",
|
||||
"repeater_duplicatesFloodDirect": "Через всю мережу: {flood}, Прямі: {direct}",
|
||||
"@repeater_duplicatesFloodDirect": {
|
||||
"placeholders": {
|
||||
"flood": {
|
||||
@@ -999,7 +1010,7 @@
|
||||
"repeater_bandwidth": "Смуга пропускання",
|
||||
"repeater_spreadingFactor": "Коефіцієнт розширення",
|
||||
"repeater_codingRate": "Швидкість кодування",
|
||||
"repeater_locationSettings": "Налаштування розташування",
|
||||
"repeater_locationSettings": "Налаштування геопозиції",
|
||||
"repeater_latitude": "Широта",
|
||||
"repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)",
|
||||
"repeater_longitude": "Довгота",
|
||||
@@ -1010,9 +1021,9 @@
|
||||
"repeater_guestAccess": "Гостьовий доступ",
|
||||
"repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання",
|
||||
"repeater_privacyMode": "Режим приватності",
|
||||
"repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях",
|
||||
"repeater_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
|
||||
"repeater_advertisementSettings": "Налаштування оголошень",
|
||||
"repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)",
|
||||
"repeater_localAdvertInterval": "Інтервал локальних оголошень (без ретрансляції)",
|
||||
"repeater_localAdvertIntervalMinutes": "{minutes} хвилин",
|
||||
"@repeater_localAdvertIntervalMinutes": {
|
||||
"placeholders": {
|
||||
@@ -1021,7 +1032,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)",
|
||||
"repeater_floodAdvertInterval": "Інтервал оголошень через всю мережу (flood)",
|
||||
"repeater_floodAdvertIntervalHours": "{hours} годин",
|
||||
"@repeater_floodAdvertIntervalHours": {
|
||||
"placeholders": {
|
||||
@@ -1035,9 +1046,9 @@
|
||||
"repeater_rebootRepeater": "Перезавантажити ретранслятор",
|
||||
"repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора",
|
||||
"repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?",
|
||||
"repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності",
|
||||
"repeater_regenerateIdentityKey": "Перегенерувати ключ ідентифікації",
|
||||
"repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентифікацію для ретранслятора. Продовжити?",
|
||||
"repeater_eraseFileSystem": "Очистити файлову систему",
|
||||
"repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора",
|
||||
"repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!",
|
||||
@@ -1146,6 +1157,7 @@
|
||||
"repeater_refreshBasicSettings": "Оновити основні налаштування",
|
||||
"repeater_refreshRadioSettings": "Оновити налаштування радіо",
|
||||
"repeater_refreshTxPower": "Оновити потужність TX",
|
||||
"repeater_refreshLocationSettings": "Оновити налаштування геопозиції",
|
||||
"repeater_refreshPacketForwarding": "Оновити пересилання пакетів",
|
||||
"repeater_refreshGuestAccess": "Оновити гостьовий доступ",
|
||||
"repeater_refreshPrivacyMode": "Оновити режим приватності",
|
||||
@@ -1201,19 +1213,19 @@
|
||||
"repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).",
|
||||
"repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.",
|
||||
"repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)",
|
||||
"repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).",
|
||||
"repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість переходів для вхідних пакетів flood (якщо >= max, пакет не пересилається).",
|
||||
"repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.",
|
||||
"repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.",
|
||||
"repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення через всю мережу. Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)",
|
||||
"repeater_cliHelpSetName": "Встановлює ім'я для оголошення.",
|
||||
"repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)",
|
||||
"repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)",
|
||||
"repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.",
|
||||
"repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).",
|
||||
"repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «через всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).",
|
||||
"repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.",
|
||||
"repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.",
|
||||
"repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.",
|
||||
@@ -1229,7 +1241,7 @@
|
||||
"repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.",
|
||||
"repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4",
|
||||
"repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.",
|
||||
"repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).",
|
||||
"repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «через всю мережу» (flood).",
|
||||
"repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.",
|
||||
"repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»",
|
||||
"repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.",
|
||||
@@ -1243,14 +1255,14 @@
|
||||
"repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.",
|
||||
"repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.",
|
||||
"repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.",
|
||||
"repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях",
|
||||
"repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.",
|
||||
"repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення геопозиції вузла:\n- none : не включати геопозицію в оголошення\n- share : ділитись геопозицією GPS (з SensorManager)\n- prefs : оголошувати геопозицію, збережену в налаштуваннях",
|
||||
"repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення геопозиції.",
|
||||
"repeater_commandsListTitle": "Список команд",
|
||||
"repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...",
|
||||
"repeater_general": "Загальні",
|
||||
"repeater_settingsCategory": "Налаштування",
|
||||
"repeater_bridge": "Міст",
|
||||
"repeater_logging": "Логування",
|
||||
"repeater_logging": "Журналювання",
|
||||
"repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)",
|
||||
"repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)",
|
||||
"repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.",
|
||||
@@ -1321,7 +1333,7 @@
|
||||
"channelPath_title": "Шлях пакету",
|
||||
"channelPath_viewMap": "Показати карту",
|
||||
"channelPath_otherObservedPaths": "Інші спостережувані шляхи",
|
||||
"channelPath_repeaterHops": "Стрибки ретранслятора",
|
||||
"channelPath_repeaterHops": "Переходи через ретранслятори",
|
||||
"channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.",
|
||||
"channelPath_messageDetails": "Деталі повідомлення",
|
||||
"channelPath_senderLabel": "Відправник",
|
||||
@@ -1340,7 +1352,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channelPath_noLocationData": "Немає даних про розташування",
|
||||
"channelPath_noLocationData": "Немає даних про геопозицію",
|
||||
"channelPath_timeWithDate": "{day}/{month} {time}",
|
||||
"@channelPath_timeWithDate": {
|
||||
"placeholders": {
|
||||
@@ -1364,9 +1376,9 @@
|
||||
}
|
||||
},
|
||||
"channelPath_unknownPath": "Невідомий",
|
||||
"channelPath_floodPath": "На всю мережу",
|
||||
"channelPath_directPath": "Прямий",
|
||||
"channelPath_observedZeroOf": "0 з {total} стрибків",
|
||||
"channelPath_floodPath": "Через всю мережу",
|
||||
"channelPath_directPath": "Напряму",
|
||||
"channelPath_observedZeroOf": "0 з {total} переходів",
|
||||
"@channelPath_observedZeroOf": {
|
||||
"placeholders": {
|
||||
"total": {
|
||||
@@ -1374,7 +1386,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channelPath_observedSomeOf": "{observed} з {total} стрибків",
|
||||
"channelPath_observedSomeOf": "{observed} з {total} переходів",
|
||||
"@channelPath_observedSomeOf": {
|
||||
"placeholders": {
|
||||
"observed": {
|
||||
@@ -1415,12 +1427,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.",
|
||||
"channelPath_noHopDetailsAvailable": "Деталі переходів недоступні для цього пакету.",
|
||||
"channelPath_unknownRepeater": "Невідомий ретранслятор",
|
||||
"listFilter_tooltip": "Фільтр та сортування",
|
||||
"listFilter_sortBy": "Сортувати за",
|
||||
"listFilter_latestMessages": "Останні повідомлення",
|
||||
"listFilter_heardRecently": "Нещодавно чули",
|
||||
"listFilter_heardRecently": "Нещодавно почуті",
|
||||
"listFilter_az": "А-Я",
|
||||
"listFilter_filters": "Фільтри",
|
||||
"listFilter_all": "Все",
|
||||
@@ -1437,20 +1449,20 @@
|
||||
}
|
||||
},
|
||||
"repeater_neighbors": "Сусіди",
|
||||
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
|
||||
"repeater_neighborsSubtitle": "Показати сусідів, доступних без ретрансляції.",
|
||||
"neighbors_receivedData": "Дані сусідів отримано",
|
||||
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
||||
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
||||
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
|
||||
"neighbors_noData": "Дані про сусідів недоступні.",
|
||||
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
||||
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
||||
"channels_joinPrivateChannel": "Приєднатись до приватного каналу",
|
||||
"channels_createPrivateChannel": "Створити приватний канал",
|
||||
"channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.",
|
||||
"channels_joinPublicChannel": "Приєднатися до публічного каналу",
|
||||
"channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.",
|
||||
"channels_joinHashtagChannel": "Приєднатися до каналу з хештегом",
|
||||
"channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.",
|
||||
"channels_joinPublicChannel": "Приєднатись до публічного каналу",
|
||||
"channels_joinPublicChannelDesc": "Будь-хто може приєднатись до цього каналу.",
|
||||
"channels_joinHashtagChannel": "Приєднатись до хештег-каналу",
|
||||
"channels_joinHashtagChannelDesc": "Будь-хто може приєднатись до хештег-каналів.",
|
||||
"channels_scanQrCode": "Сканувати QR-код",
|
||||
"channels_scanQrCodeComingSoon": "Скоро буде",
|
||||
"channels_enterHashtag": "Введіть хештег",
|
||||
@@ -1472,7 +1484,7 @@
|
||||
"neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}",
|
||||
"neighbors_heardAgo": "Почуто: {time} тому",
|
||||
"settings_locationGPSEnable": "Увімкнути GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.",
|
||||
"settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення геопозиції через GPS.",
|
||||
"settings_locationIntervalSec": "Інтервал для GPS (Секунди)",
|
||||
"settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.",
|
||||
"contacts_manageRoom": "Керувати сервером кімнати",
|
||||
@@ -1536,10 +1548,10 @@
|
||||
"common_ok": "ОК",
|
||||
"community_title": "Спільнота",
|
||||
"community_create": "Створити спільноту",
|
||||
"community_createDesc": "Створити нову спільноту та поділитися через QR-код.",
|
||||
"community_join": "Приєднатися",
|
||||
"community_joinTitle": "Приєднатися до спільноти",
|
||||
"community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?",
|
||||
"community_createDesc": "Створити нову спільноту та поділитись через QR-код.",
|
||||
"community_join": "Приєднатись",
|
||||
"community_joinTitle": "Приєднатись до спільноти",
|
||||
"community_joinConfirmation": "Ви бажаєте приєднатись до спільноти «{name}»?",
|
||||
"community_scanQr": "Сканувати QR спільноти",
|
||||
"community_scanInstructions": "Наведіть камеру на QR-код спільноти.",
|
||||
"community_showQr": "Показати QR-код",
|
||||
@@ -1549,9 +1561,9 @@
|
||||
"community_enterName": "Введіть назву спільноти",
|
||||
"community_created": "Спільноту «{name}» створено",
|
||||
"community_joined": "Приєднався до спільноти «{name}»",
|
||||
"community_qrTitle": "Поділитися спільнотою",
|
||||
"community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}",
|
||||
"community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти",
|
||||
"community_qrTitle": "Поділитись спільнотою",
|
||||
"community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатись до {name}",
|
||||
"community_hashtagPrivacyHint": "Хештег-канали спільноти доступні лише членам спільноти",
|
||||
"community_invalidQrCode": "Недійсний QR-код спільноти",
|
||||
"community_alreadyMember": "Вже учасник",
|
||||
"community_alreadyMemberMessage": "Ви вже є учасником «{name}».",
|
||||
@@ -1572,10 +1584,10 @@
|
||||
},
|
||||
"community_deleted": "Спільноту «{name}» покинуто",
|
||||
"community_addHashtagChannel": "Додати хештег спільноти",
|
||||
"community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти",
|
||||
"community_addHashtagChannelDesc": "Додати хештег-канал для цієї спільноти",
|
||||
"community_selectCommunity": "Вибрати спільноту",
|
||||
"community_regularHashtag": "Звичайний хештег",
|
||||
"community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)",
|
||||
"community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатись)",
|
||||
"community_communityHashtag": "Хештег спільноти",
|
||||
"community_communityHashtagDesc": "Ексклюзивно для членів спільноти",
|
||||
"community_forCommunity": "Для {name}",
|
||||
@@ -1607,13 +1619,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerateSecret": "Перегенерувати секрет",
|
||||
"community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.",
|
||||
"community_regenerateSecret": "Перегенерувати секретний ключ",
|
||||
"community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Усі учасники матимуть відсканувати новий QR-код, щоб продовжити спілкування.",
|
||||
"community_regenerate": "Перегенерувати",
|
||||
"community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано",
|
||||
"community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»",
|
||||
"community_updateSecret": "Оновити секрет",
|
||||
"community_secretUpdated": "Зміну секрету для «{name}» оновлено",
|
||||
"community_secretRegenerated": "Секретний ключ для «{name}» перегенеровано",
|
||||
"community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити секретний ключ для «{name}»",
|
||||
"community_updateSecret": "Оновити секретний ключ",
|
||||
"community_secretUpdated": "Секретний ключ для «{name}» оновлено",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -1622,76 +1634,76 @@
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Ви",
|
||||
"pathTrace_failed": "Відстеження шляху не вдалося.",
|
||||
"pathTrace_failed": "Відстеження шляху не вдалось.",
|
||||
"pathTrace_notAvailable": "Трасування шляху недоступне.",
|
||||
"pathTrace_refreshTooltip": "Оновити Path Trace",
|
||||
"pathTrace_refreshTooltip": "Оновити трасування шляху",
|
||||
"contacts_pathTrace": "Трасування шляхів",
|
||||
"contacts_ping": "Пінгувати",
|
||||
"contacts_repeaterPathTrace": "Трасування шляху до повторювача",
|
||||
"contacts_repeaterPing": "Пінгувати повторювач",
|
||||
"contacts_repeaterPathTrace": "Трасування шляху до ретранслятора",
|
||||
"contacts_repeaterPing": "Пінгувати ретранслятор",
|
||||
"contacts_roomPathTrace": "Трасування шляху до серверу кімнати",
|
||||
"contacts_roomPing": "Пінг сервера кімнати",
|
||||
"contacts_chatTraceRoute": "Трасування шляху",
|
||||
"contacts_pathTraceTo": "Відстежити маршрут до {name}",
|
||||
"contacts_invalidAdvertFormat": "Недійсні контактні дані",
|
||||
"contacts_contactImported": "Контакт було імпортовано.",
|
||||
"contacts_contactImportFailed": "Контакт не вдалося імпортувати",
|
||||
"contacts_zeroHopAdvert": "Реклама без перехоплення",
|
||||
"contacts_floodAdvert": "Залив реклами",
|
||||
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
||||
"contacts_contactImportFailed": "Контакт не вдалось імпортувати",
|
||||
"contacts_zeroHopAdvert": "Оголошення без ретрансляції",
|
||||
"contacts_floodAdvert": "Оголошення з ретрансляцією",
|
||||
"contacts_copyAdvertToClipboard": "Копіювати оголошення",
|
||||
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
||||
"appSettings_languageRu": "Російська",
|
||||
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
|
||||
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
|
||||
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
||||
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
||||
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
||||
"contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало",
|
||||
"contacts_zeroHopContactAdvertFailed": "Не вдалось надіслати контакт.",
|
||||
"contacts_contactAdvertCopied": "Оголошення скопійовано до буфера обміну.",
|
||||
"contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилось невдало",
|
||||
"contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням",
|
||||
"contacts_addContactFromClipboard": "Додати контакт з буфера обміну",
|
||||
"contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням",
|
||||
"contacts_addContactFromClipboard": "Додати контакт з буфера",
|
||||
"contacts_ShareContactZeroHop": "Поділитись контактом за оголошенням",
|
||||
"notification_activityTitle": "Активність MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нові вузли} many{нових вузлів} other{нових вузлів}}",
|
||||
"notification_newTypeDiscovered": "Виявлено новий {contactType}",
|
||||
"notification_receivedNewMessage": "Отримано нове повідомлення",
|
||||
"settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.",
|
||||
"settings_gpxExportRepeaters": "Експорт ретрансляторів і серверів кімнат у GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори та сервери кімнат з геопозицією у файл GPX.",
|
||||
"settings_gpxExportSuccess": "Успішно експортовано файл GPX.",
|
||||
"settings_gpxExportNoContacts": "Немає контактів для експорту.",
|
||||
"settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі",
|
||||
"settings_gpxExportError": "Сталася помилка під час експорту.",
|
||||
"settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.",
|
||||
"settings_gpxExportAll": "Експортувати всі контакти до GPX",
|
||||
"settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.",
|
||||
"settings_gpxExportContacts": "Експортувати супутників до GPX",
|
||||
"settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати",
|
||||
"settings_gpxExportChat": "Місця супутників",
|
||||
"settings_gpxExportError": "Сталась помилка під час експорту.",
|
||||
"settings_gpxExportAllSubtitle": "Експортує всі контакти з геопозицією у файл GPX.",
|
||||
"settings_gpxExportAll": "Експорт усіх контактів у GPX",
|
||||
"settings_gpxExportContactsSubtitle": "Експортує контакти з геопозицією у файл GPX.",
|
||||
"settings_gpxExportContacts": "Експорт контактів у GPX",
|
||||
"settings_gpxExportRepeatersRoom": "Геопозиції ретрансляторів та серверів кімнат",
|
||||
"settings_gpxExportChat": "Геопозиції контактів",
|
||||
"settings_gpxExportShareText": "Дані карти експортовані з meshcore-open",
|
||||
"settings_gpxExportAllContacts": "Усі місця контактів",
|
||||
"settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX",
|
||||
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!",
|
||||
"pathTrace_someHopsNoLocation": "Один або декілька переходів не мають даних про геопозицію!",
|
||||
"map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху",
|
||||
"map_runTrace": "Виконати трасування шляху",
|
||||
"pathTrace_clearTooltip": "Очистити шлях",
|
||||
"map_removeLast": "Видалити останній",
|
||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||
"map_pathTraceCancelled": "Трасування шляху скасовано.",
|
||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||
"scanner_chromeRequired": "Потрібен браузер Chrome",
|
||||
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
|
||||
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому вебзастосунку потрібен Google Chrome або браузер на базі Chromium.",
|
||||
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||
"snrIndicator_nearByRepeaters": "Найближчі ретранслятори",
|
||||
"chat_ShowAllPaths": "Показати всі шляхи",
|
||||
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
||||
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
||||
"settings_clientRepeat": "Автономна система",
|
||||
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "одиниці",
|
||||
"appSettings_unitsMetric": "Метричний (м / км)",
|
||||
"appSettings_unitsImperial": "Імперська (ft / mi)",
|
||||
"appSettings_unitsTitle": "Одиниці",
|
||||
"appSettings_unitsMetric": "Метричні (м / км)",
|
||||
"appSettings_unitsImperial": "Імперські (ft / mi)",
|
||||
"map_lineOfSight": "Пряма видимість",
|
||||
"map_losScreenTitle": "Пряма видимість",
|
||||
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
|
||||
@@ -1709,7 +1721,7 @@
|
||||
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
|
||||
"losShowDisplayNodes": "Показати вузли відображення",
|
||||
"losCustomPoints": "Користувальницькі точки",
|
||||
"losCustomPointLabel": "Спеціальний {index}",
|
||||
"losCustomPointLabel": "Власна точка {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
@@ -1798,7 +1810,7 @@
|
||||
},
|
||||
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
|
||||
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
|
||||
"losRenameCustomPoint": "Перейменуйте спеціальну точку",
|
||||
"losRenameCustomPoint": "Перейменувати власну точку",
|
||||
"losPointName": "Назва точки",
|
||||
"losShowPanelTooltip": "Показати панель LOS",
|
||||
"losHidePanelTooltip": "Приховати панель LOS",
|
||||
@@ -1878,19 +1890,19 @@
|
||||
"contacts_unread": "Непрочитане",
|
||||
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
|
||||
"settings_contactSettings": "Налаштування контактів",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлених користувачів",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати ретранслятори",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені ретранслятори",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
|
||||
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
|
||||
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
|
||||
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
|
||||
"contactsSettings_title": "Налаштування контактів",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сервери кімнат.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати сенсори",
|
||||
"discoveredContacts_searchHint": "Знайти виявлені контакти",
|
||||
"discoveredContacts_contactAdded": "Контакт додано",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сенсори",
|
||||
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
|
||||
"discoveredContacts_Title": "Виявлені контакти",
|
||||
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
|
||||
@@ -1901,9 +1913,9 @@
|
||||
"common_deleteAll": "Видалити все",
|
||||
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
|
||||
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
|
||||
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
|
||||
"map_guessedLocation": "Визначено місцезнаходження",
|
||||
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||
"map_showGuessedLocations": "Показати геопозиції передбачених вузлів",
|
||||
"map_guessedLocation": "Передбачена геопозиція",
|
||||
"usbScreenSubtitle": "Виберіть виявлений USB-пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||
"usbScreenTitle": "Підключити через USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
@@ -1915,8 +1927,8 @@
|
||||
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
|
||||
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
|
||||
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
|
||||
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
|
||||
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
|
||||
"usbErrorOpenFailed": "Не вдалось відкрити вибране USB-пристрій.",
|
||||
"usbErrorConnectFailed": "Не вдалось підключитись до вибраного USB-пристрою.",
|
||||
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
|
||||
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
||||
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
||||
@@ -1931,9 +1943,9 @@
|
||||
},
|
||||
"usbStatus_searching": "Пошук пристроїв USB...",
|
||||
"usbStatus_notConnected": "Виберіть пристрій USB",
|
||||
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
||||
"usbConnectionFailed": "Не вдалось встановити з'єднання через USB: {error}",
|
||||
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
||||
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||
"usbErrorConnectTimedOut": "З'єднання не вдалось встановити. Переконайтесь, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
@@ -1951,18 +1963,18 @@
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpHostLabel": "IP-адреса",
|
||||
"tcpScreenTitle": "З'єднатися через протокол TCP",
|
||||
"tcpScreenTitle": "Підключитись через TCP",
|
||||
"tcpPortLabel": "Порт",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Введіть кінцеву точку та підключіться",
|
||||
"tcpStatus_connectingTo": "Підключення до {endpoint}...",
|
||||
"tcpErrorHostRequired": "Необхідно вказати IP-адресу.",
|
||||
"tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.",
|
||||
"tcpErrorPortInvalid": "Порт має бути в межах від 1 до 65535.",
|
||||
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
|
||||
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
|
||||
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Показати контакти Відкриття",
|
||||
"map_setAsMyLocation": "Встановити моє місцезнаходження",
|
||||
"tcpErrorTimedOut": "З'єднання TCP завершилось через закінчення часу очікування.",
|
||||
"tcpConnectionFailed": "Не вдалось встановити з'єднання TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Показати виявлені контакти",
|
||||
"map_setAsMyLocation": "Встановити мою геопозицію",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
@@ -1973,12 +1985,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
|
||||
"settings_privacySubtitle": "Керуйте інформацією, яка буде спільно використовуватись",
|
||||
"settings_privacy": "Налаштування приватності",
|
||||
"settings_telemetryBaseMode": "Режим базової телеметрії",
|
||||
"settings_telemetryLocationMode": "Режим місця телеметрії",
|
||||
"settings_advertLocation": "Розміщення реклами",
|
||||
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
|
||||
"settings_advertLocation": "Геопозиція в оголошенні",
|
||||
"settings_advertLocationSubtitle": "Включити геопозицію в оголошення",
|
||||
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
|
||||
"settings_allowAll": "Дозволити все",
|
||||
"settings_denyAll": "Відхилити все",
|
||||
@@ -1986,8 +1998,8 @@
|
||||
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
|
||||
"contact_info": "Контактна інформація",
|
||||
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
|
||||
"contact_teleLoc": "Розташування телеметрії",
|
||||
"contact_teleBase": "Базовий телебачення",
|
||||
"contact_teleLoc": "Геопозиція телеметрії",
|
||||
"contact_teleBase": "Базова телеметрія",
|
||||
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
|
||||
"contact_settings": "Налаштування контактів",
|
||||
"contact_telemetry": "Телеметрія",
|
||||
@@ -1995,13 +2007,6 @@
|
||||
"contact_lastSeen": "Останній раз бачили",
|
||||
"contact_teleEnv": "Середовище телеметрії",
|
||||
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Початкова вартість маршруту",
|
||||
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
|
||||
"appSettings_maxRouteWeight": "Максимальна вага маршруту",
|
||||
@@ -2014,9 +2019,8 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
|
||||
"settings_multiAck": "Багатократне підтвердження: {value}",
|
||||
"map_showOverlaps": "Перекриття ключа повторювача",
|
||||
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом",
|
||||
"map_showOverlaps": "Перекриття ключів ретрансляторів",
|
||||
"map_runTraceWithReturnPath": "Повернутись назад тим же шляхом",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
@@ -2060,26 +2064,26 @@
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.",
|
||||
"appSettings_languageHu": "Угорський",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.",
|
||||
"appSettings_languageHu": "Угорська",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з непрочитаними повідомленнями, прокрутіть до першого непрочитаного повідомлення, а не до останнього.",
|
||||
"appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення",
|
||||
"appSettings_languageJa": "Японська",
|
||||
"appSettings_languageKo": "Кореєська",
|
||||
"appSettings_languageKo": "Корейська",
|
||||
"radioStats_tooltip": "Статистика радіо та мережі",
|
||||
"radioStats_screenTitle": "Дані про радіостанції",
|
||||
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.",
|
||||
"radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.",
|
||||
"radioStats_waiting": "Очікую на отримання даних…",
|
||||
"radioStats_screenTitle": "Статистика радіо",
|
||||
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіо.",
|
||||
"radioStats_firmwareTooOld": "Статистика радіо вимагає прошивки пристрою-компаньйона версії 8 або новішої.",
|
||||
"radioStats_waiting": "Очікування даних…",
|
||||
"radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм",
|
||||
"radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм",
|
||||
"radioStats_lastSnr": "Останній показник SNR: {snr} дБ",
|
||||
"radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд",
|
||||
"radioStats_rxAir": "Загальний час використання RX: {seconds} секунд",
|
||||
"radioStats_txAir": "Час в ефірі TX (загальний): {seconds} секунд",
|
||||
"radioStats_rxAir": "Час в ефірі RX (загальний): {seconds} секунд",
|
||||
"radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.",
|
||||
"radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм",
|
||||
"radioStats_stripWaiting": "Отримано статистику радіо…",
|
||||
"radioStats_settingsTile": "Дані про радіостанції",
|
||||
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.",
|
||||
"radioStats_settingsTile": "Статистика радіо",
|
||||
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час в ефірі.",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
@@ -2093,9 +2097,9 @@
|
||||
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
|
||||
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
|
||||
"translation_targetLanguage": "Цільова мова",
|
||||
"translation_useAppLanguage": "Використовуйте мову додатку",
|
||||
"translation_useAppLanguage": "Використовувати мову застосунку",
|
||||
"translation_downloadedModelLabel": "Завантажений шаблон",
|
||||
"translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face",
|
||||
"translation_presetModelLabel": "Попередньо налаштована модель з Hugging Face",
|
||||
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
|
||||
"translation_downloadModel": "Завантажити модель",
|
||||
"translation_downloading": "Завантаження...",
|
||||
@@ -2106,7 +2110,7 @@
|
||||
"translation_deleteModel": "Видалити модель",
|
||||
"translation_modelDownloaded": "Модель перекладу завантажена.",
|
||||
"translation_downloadStopped": "Завантаження призупинено.",
|
||||
"translation_downloadFailed": "Не вдалося завантажити: {error}",
|
||||
"translation_downloadFailed": "Не вдалось завантажити: {error}",
|
||||
"translation_enterUrlFirst": "Спочатку введіть URL моделі.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
@@ -2134,7 +2138,7 @@
|
||||
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
|
||||
"scanner_linuxPairingHidePin": "Приховати PIN",
|
||||
"repeater_cliQuickClockSync": "Синхронізація годинника",
|
||||
"repeater_cliQuickDiscovery": "Відкрити сусідів",
|
||||
"repeater_cliQuickDiscovery": "Виявити сусідів",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
@@ -2143,7 +2147,7 @@
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
|
||||
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
|
||||
"repeater_guestTools": "Інструменти для гостей",
|
||||
"repeater_guestTools": "Гостьові інструменти",
|
||||
"repeater_guest": "Інформація про ретранслятор",
|
||||
"room_guest": "Інформація про сервер кімнати",
|
||||
"chat_sendMessage": "Надіслати повідомлення",
|
||||
@@ -2217,5 +2221,57 @@
|
||||
"repeater_cliHelpRegionListDenied": "Перелік регіонів, які забороняють рух транспорту під час повені.",
|
||||
"repeater_cliHelpStatsPackets": "(Тільки для серійного використання) Відображає статистику на рівні пакетів.",
|
||||
"repeater_cliHelpStatsRadio": "(Тільки для серій) Відображає радіостатистику.",
|
||||
"repeater_cliHelpStatsCore": "(Тільки для серійного використання) Відображає основні статистичні дані про програмне забезпечення."
|
||||
"repeater_cliHelpStatsCore": "(Тільки для серійного використання) Відображає основні статистичні дані про програмне забезпечення.",
|
||||
"background_serviceTitle": "MeshCore працює",
|
||||
"background_serviceText": "Підтримує з'єднання BLE",
|
||||
"appSettings_translationModelDeleted": "Видалено {name}",
|
||||
"appSettings_translationModelDeleteFailed": "Не вдалось видалити: {error}",
|
||||
"channels_channelUpdateFailed": "Не вдалось оновити канал: {error}",
|
||||
"contact_typeChat": "Чат",
|
||||
"contact_typeRepeater": "Ретранслятор",
|
||||
"contact_typeRoom": "Кімната",
|
||||
"contact_typeSensor": "Сенсор",
|
||||
"contact_typeUnknown": "Невідомо",
|
||||
"settings_multiAck": "Багато підтверджень",
|
||||
"map_sharedAt": "Поділено",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Натисніть на заблоковане місце, щоб виділити його на карті.",
|
||||
"losBlockedSpotsTitle": "Заблоковані місця",
|
||||
"losSelectedObstructionTitle": "Вибраний об'єкт перешкоди",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+85
-9
@@ -109,6 +109,8 @@
|
||||
"settings_privacyModeEnabled": "隐私模式已启用",
|
||||
"settings_privacyModeDisabled": "隐私模式已关闭",
|
||||
"settings_actions": "操作",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "发送广播",
|
||||
"settings_sendAdvertisementSubtitle": "立即发送广播",
|
||||
"settings_advertisementSent": "已发送广播",
|
||||
@@ -2000,13 +2002,6 @@
|
||||
"contact_settings": "联系人设置",
|
||||
"contact_teleLocSubtitle": "允许共享位置数据",
|
||||
"contact_telemetry": "遥测数据",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "最大路径重量",
|
||||
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
|
||||
"appSettings_initialRouteWeight": "初始路线权重",
|
||||
@@ -2018,7 +2013,7 @@
|
||||
"appSettings_maxMessageRetries": "最大消息重试次数",
|
||||
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "多重ACK:{value}",
|
||||
"settings_multiAck": "多重ACK",
|
||||
"settings_telemetryModeUpdated": "遥测模式已更新",
|
||||
"map_showOverlaps": "重复键重叠",
|
||||
"map_runTraceWithReturnPath": "沿着相同的路径返回",
|
||||
@@ -2222,5 +2217,86 @@
|
||||
"repeater_cliHelpRegionListDenied": "列出禁止洪水交通的区域。",
|
||||
"repeater_cliHelpStatsPackets": "(仅显示序列信息)显示数据包级别的统计信息。",
|
||||
"repeater_cliHelpStatsRadio": "(仅显示序列信息)显示收音机相关统计数据。",
|
||||
"repeater_cliHelpStatsCore": "(仅显示序列号)显示核心固件统计信息。"
|
||||
"repeater_cliHelpStatsCore": "(仅显示序列号)显示核心固件统计信息。",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "已分享",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "被占用区域",
|
||||
"losBlockedSpotsHint": "点击地图上的某个被遮盖的区域,以突出显示该区域。",
|
||||
"losSelectedObstructionTitle": "选择性阻碍",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'app_localizations.dart';
|
||||
|
||||
/// UI-level localization helpers for [Contact].
|
||||
///
|
||||
/// Kept out of the model layer so `Contact` does not depend on
|
||||
/// `AppLocalizations`. Use these from widgets/screens; for logs and
|
||||
/// non-UI export use `Contact.typeLabelRaw`.
|
||||
extension ContactLocalization on Contact {
|
||||
String typeLabel(AppLocalizations l10n) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return l10n.contact_typeChat;
|
||||
case advTypeRepeater:
|
||||
return l10n.contact_typeRepeater;
|
||||
case advTypeRoom:
|
||||
return l10n.contact_typeRoom;
|
||||
case advTypeSensor:
|
||||
return l10n.contact_typeSensor;
|
||||
default:
|
||||
return l10n.contact_typeUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
String pathLabel(AppLocalizations l10n) {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return l10n.chat_floodForced;
|
||||
if (pathOverride == 0) return l10n.chat_directForced;
|
||||
return l10n.chat_hopsForced(pathOverride!);
|
||||
}
|
||||
if (pathLength < 0) return l10n.channelPath_floodPath;
|
||||
if (pathLength == 0) return l10n.chat_direct;
|
||||
return l10n.chat_hopsCount(pathLength);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ void main() async {
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.initialize();
|
||||
await backgroundService.initialize();
|
||||
backgroundService.setLanguageOverrideProvider(
|
||||
() => appSettingsService.settings.languageOverride,
|
||||
);
|
||||
_registerThirdPartyLicenses();
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
|
||||
+32
-15
@@ -17,6 +17,7 @@ class Contact {
|
||||
final double? longitude;
|
||||
final DateTime lastSeen;
|
||||
final DateTime lastMessageAt;
|
||||
final DateTime? lastModified;
|
||||
final bool isActive;
|
||||
final bool wasPulled;
|
||||
final Uint8List? rawPacket;
|
||||
@@ -33,6 +34,7 @@ class Contact {
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.lastSeen,
|
||||
this.lastModified,
|
||||
DateTime? lastMessageAt,
|
||||
this.isActive = true,
|
||||
this.wasPulled = false,
|
||||
@@ -41,7 +43,10 @@ class Contact {
|
||||
|
||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||
|
||||
String get typeLabel {
|
||||
/// Non-localized type label, intended for logs and non-UI exports
|
||||
/// (e.g. GPX). For UI use the `typeLabel(l10n)` extension in
|
||||
/// `lib/l10n/contact_localization.dart`.
|
||||
String get typeLabelRaw {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return 'Chat';
|
||||
@@ -56,17 +61,6 @@ class Contact {
|
||||
}
|
||||
}
|
||||
|
||||
String get pathLabel {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return 'Flood (forced)';
|
||||
if (pathOverride == 0) return 'Direct (forced)';
|
||||
return '$pathOverride hops (forced)';
|
||||
}
|
||||
if (pathLength < 0) return 'Flood';
|
||||
if (pathLength == 0) return 'Direct';
|
||||
return '$pathLength hops';
|
||||
}
|
||||
|
||||
bool get hasLocation {
|
||||
const double epsilon = 1e-6;
|
||||
final lat = latitude ?? 0.0;
|
||||
@@ -94,6 +88,7 @@ class Contact {
|
||||
double? longitude,
|
||||
DateTime? lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
DateTime? lastModified,
|
||||
bool? isActive,
|
||||
Uint8List? rawPacket,
|
||||
}) {
|
||||
@@ -114,6 +109,7 @@ class Contact {
|
||||
longitude: longitude ?? this.longitude,
|
||||
lastSeen: lastSeen ?? this.lastSeen,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
isActive: isActive ?? this.isActive,
|
||||
rawPacket: rawPacket ?? this.rawPacket,
|
||||
);
|
||||
@@ -182,16 +178,34 @@ class Contact {
|
||||
return null;
|
||||
}
|
||||
|
||||
final lastMod = reader.readUInt32LE();
|
||||
// mandatory last_advert_timestamp
|
||||
final lastAdvertTimestamp = reader.readUInt32LE();
|
||||
|
||||
double? lat, lon;
|
||||
if (reader.remaining >= 8) {
|
||||
DateTime? lastModified;
|
||||
if (reader.remaining >= 12) {
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
final lastModRaw = reader.readUInt32LE();
|
||||
// TODO: should this be &&?
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
if (lastModRaw != 0) {
|
||||
lastModified = DateTime.fromMillisecondsSinceEpoch(lastModRaw * 1000);
|
||||
}
|
||||
} else if (reader.remaining >= 8) {
|
||||
// Old layout: gps without lastmod
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
appLogger.info(
|
||||
'Contact ${pubKeyToHex(pubKey).substring(0, 8)} has gps but no lastmod (legacy firmware layout)',
|
||||
);
|
||||
}
|
||||
|
||||
return Contact(
|
||||
@@ -203,7 +217,10 @@ class Contact {
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastAdvertTimestamp * 1000,
|
||||
),
|
||||
lastModified: lastModified,
|
||||
isActive: true,
|
||||
rawPacket: null,
|
||||
);
|
||||
|
||||
@@ -228,7 +228,7 @@ class RadioSettings {
|
||||
frequencyMHz: 433.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
@@ -238,7 +238,7 @@ class RadioSettings {
|
||||
frequencyMHz: 869.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
@@ -248,7 +248,7 @@ class RadioSettings {
|
||||
frequencyMHz: 918.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1237,15 +1237,20 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
// TODO: l10n
|
||||
content: Text('Deleted ${translationModelFriendlyName(model)}.'),
|
||||
content: Text(
|
||||
context.l10n.appSettings_translationModelDeleted(
|
||||
translationModelFriendlyName(model),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text('Delete failed: $error'),
|
||||
); // TODO: l10n
|
||||
content: Text(
|
||||
context.l10n.appSettings_translationModelDeleteFailed('$error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
@@ -13,7 +13,6 @@ import '../helpers/chat_scroll_controller.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel.dart';
|
||||
@@ -23,6 +22,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../widgets/byte_count_input.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
@@ -32,13 +32,19 @@ import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../widgets/unread_divider.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
|
||||
class ChannelChatScreen extends StatefulWidget {
|
||||
final Channel channel;
|
||||
final int initialUnreadCount;
|
||||
|
||||
const ChannelChatScreen({super.key, required this.channel});
|
||||
const ChannelChatScreen({
|
||||
super.key,
|
||||
required this.channel,
|
||||
this.initialUnreadCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||
@@ -55,32 +61,46 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChannelSendAt;
|
||||
bool _channelSkipNextBottomSnap = false;
|
||||
String? _unreadDividerMessageId;
|
||||
|
||||
String? _cachedFormatLocale;
|
||||
late DateFormat _hmFormat;
|
||||
late DateFormat _mdFormat;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final idx = widget.channel.index;
|
||||
final unread = connector.getUnreadCountForChannelIndex(idx);
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
ChannelMessage? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(
|
||||
connector.getChannelMessages(widget.channel),
|
||||
unread,
|
||||
);
|
||||
if (unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(messages, unread);
|
||||
}
|
||||
setState(() {
|
||||
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
|
||||
});
|
||||
connector.setActiveChannel(idx);
|
||||
_connector = connector;
|
||||
if (anchor != null) {
|
||||
if (anchor != null && settings.jumpToOldestUnread) {
|
||||
_channelSkipNextBottomSnap = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
_scrollController.jumpToEstimatedOffset(
|
||||
unreadCount: unread,
|
||||
totalMessages: messages.length,
|
||||
onJumped: () {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -102,6 +122,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void _clearDividerAtBottom() {
|
||||
if (!_scrollController.showJumpToBottom.value &&
|
||||
_unreadDividerMessageId != null) {
|
||||
setState(() => _unreadDividerMessageId = null);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTextFieldFocusChange() {
|
||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||
_scrollController.handleKeyboardOpen();
|
||||
@@ -123,6 +150,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_connector?.setActiveChannel(null);
|
||||
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
|
||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||
_textFieldFocusNode.dispose();
|
||||
_textController.dispose();
|
||||
@@ -321,6 +349,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
if (!_messageKeys.containsKey(message.messageId)) {
|
||||
_messageKeys[message.messageId] = GlobalKey();
|
||||
}
|
||||
final isUnreadAnchor =
|
||||
_unreadDividerMessageId != null &&
|
||||
message.messageId == _unreadDividerMessageId;
|
||||
return Container(
|
||||
key: _messageKeys[message.messageId]!,
|
||||
child: Builder(
|
||||
@@ -329,10 +360,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
return _buildMessageBubble(
|
||||
final bubble = _buildMessageBubble(
|
||||
message,
|
||||
textScale,
|
||||
);
|
||||
if (isUnreadAnchor) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const UnreadDivider(), bubble],
|
||||
);
|
||||
}
|
||||
return bubble;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -352,12 +390,24 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _markAsUnread(ChannelMessage message) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
var count = 0;
|
||||
var found = false;
|
||||
for (final m in messages) {
|
||||
if (m.messageId == message.messageId) found = true;
|
||||
if (found && !m.isOutgoing) count++;
|
||||
}
|
||||
connector.setChannelUnreadCount(widget.channel.index, count);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final gifId = GifHelper.parseGif(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
final poi = parseMarkerText(message.text);
|
||||
final translatedDisplayText =
|
||||
message.translatedText != null &&
|
||||
message.translatedText!.trim().isNotEmpty
|
||||
@@ -445,6 +495,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
poi,
|
||||
isOutgoing,
|
||||
textScale,
|
||||
message.senderName,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
@@ -555,7 +606,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
'via ${_formatPathPrefixes(displayPath)}',
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(displayPath),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
@@ -576,7 +629,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
_formatTime(context, message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
@@ -701,7 +754,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
|
||||
|
||||
final gifId = GifHelper.parseGif(replyText);
|
||||
final poi = _parsePoiMessage(replyText);
|
||||
final poi = parseMarkerText(replyText);
|
||||
|
||||
Widget contentPreview;
|
||||
if (gifId != null) {
|
||||
@@ -812,24 +865,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
_PoiInfo? _parsePoiMessage(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(
|
||||
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|',
|
||||
).firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final lat = double.tryParse(match.group(1) ?? '');
|
||||
final lon = double.tryParse(match.group(2) ?? '');
|
||||
if (lat == null || lon == null) return null;
|
||||
final label = match.group(3) ?? '';
|
||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
||||
}
|
||||
|
||||
Widget _buildPoiMessage(
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
MarkerPayload poi,
|
||||
bool isOutgoing,
|
||||
double textScale, {
|
||||
double textScale,
|
||||
String senderName, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -849,12 +890,22 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
onPressed: () {
|
||||
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
|
||||
final fromName = isOutgoing ? selfName : senderName;
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: 'channel:${widget.channel.index}',
|
||||
label: poi.label,
|
||||
fromName: fromName,
|
||||
flags: poi.flags,
|
||||
isChannel: true,
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapScreen(
|
||||
highlightPosition: LatLng(poi.lat, poi.lon),
|
||||
highlightPosition: poi.position,
|
||||
highlightLabel: poi.label,
|
||||
highlightMarkerKey: key,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1093,27 +1144,33 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
return ByteCountedTextField(
|
||||
maxBytes: maxBytes,
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
encoder:
|
||||
connector.isChannelSmazEnabled(widget.channel.index)
|
||||
? (text) => connector.prepareChannelOutboundText(
|
||||
widget.channel.index,
|
||||
text,
|
||||
)
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
horizontal: 20,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -1195,7 +1252,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
if (utf8.encode(messageText).length > maxBytes) {
|
||||
final outboundText = connector.prepareChannelOutboundText(
|
||||
widget.channel.index,
|
||||
messageText,
|
||||
);
|
||||
if (utf8.encode(outboundText).length > maxBytes) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
|
||||
@@ -1215,14 +1276,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
String _formatTime(BuildContext context, DateTime time) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(time);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
if (locale != _cachedFormatLocale) {
|
||||
_cachedFormatLocale = locale;
|
||||
_hmFormat = DateFormat.Hm(locale);
|
||||
_mdFormat = DateFormat.Md(locale);
|
||||
}
|
||||
final hm = _hmFormat.format(time);
|
||||
|
||||
if (diff.inDays > 0) {
|
||||
return '${time.day}/${time.month} ${time.hour}:${time.minute.toString().padLeft(2, '0')}';
|
||||
return '${_mdFormat.format(time)} $hm';
|
||||
} else {
|
||||
return '${time.hour}:${time.minute.toString().padLeft(2, '0')}';
|
||||
return hm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,6 +1346,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_copyMessageText(message.text);
|
||||
},
|
||||
),
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
title: Text(context.l10n.chat_markAsUnread),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_markAsUnread(message);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: Text(context.l10n.common_delete),
|
||||
@@ -1497,11 +1574,3 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PoiInfo {
|
||||
final double lat;
|
||||
final double lon;
|
||||
final String label;
|
||||
|
||||
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -304,6 +304,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||
class _ChannelMessagePathMapScreenState
|
||||
extends State<ChannelMessagePathMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
Uint8List? _selectedPath;
|
||||
@@ -330,6 +332,18 @@ class _ChannelMessagePathMapScreenState
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
double _getPathDistance(List<LatLng> points) {
|
||||
double totalDistance = 0.0;
|
||||
final distanceCalculator = Distance();
|
||||
@@ -357,6 +371,70 @@ class _ChannelMessagePathMapScreenState
|
||||
});
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(initialCenter, initialZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: 16,
|
||||
top: 16,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
@@ -372,6 +450,7 @@ class _ChannelMessagePathMapScreenState
|
||||
primaryPath,
|
||||
widget.message.pathVariants,
|
||||
);
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final selectedPathTmp = _resolveSelectedPath(
|
||||
_selectedPath,
|
||||
observedPaths,
|
||||
@@ -451,10 +530,20 @@ class _ChannelMessagePathMapScreenState
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
@@ -486,6 +575,12 @@ class _ChannelMessagePathMapScreenState
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
if (observedPaths.length > 1)
|
||||
_buildPathSelector(context, observedPaths, selectedIndex, (
|
||||
index,
|
||||
|
||||
@@ -492,13 +492,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final unread = connector.getUnreadCountForChannelIndex(
|
||||
channel.index,
|
||||
);
|
||||
connector.markChannelRead(channel.index);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelChatScreen(channel: channel),
|
||||
builder: (context) => ChannelChatScreen(
|
||||
channel: channel,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1492,7 +1498,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text('Failed to update channel: $e'),
|
||||
content: Text(
|
||||
context.l10n.channels_channelUpdateFailed('$e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+142
-65
@@ -9,7 +9,6 @@ import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
@@ -18,9 +17,9 @@ import '../widgets/message_status_icon.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../models/translation_support.dart';
|
||||
@@ -30,6 +29,7 @@ import '../services/path_history_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/elements_ui.dart';
|
||||
import '../widgets/byte_count_input.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
@@ -44,12 +44,18 @@ import '../widgets/translated_message_content.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../widgets/unread_divider.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
final int initialUnreadCount;
|
||||
|
||||
const ChatScreen({super.key, required this.contact});
|
||||
const ChatScreen({
|
||||
super.key,
|
||||
required this.contact,
|
||||
this.initialUnreadCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
@@ -63,6 +69,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
bool _isLoadingOlder = false;
|
||||
MeshCoreConnector? _connector;
|
||||
Message? _pendingUnreadScrollTarget;
|
||||
String? _unreadDividerMessageId;
|
||||
DateTime? _lastTextSendAt;
|
||||
|
||||
@override
|
||||
@@ -70,34 +77,47 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
super.initState();
|
||||
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final keyHex = widget.contact.publicKeyHex;
|
||||
final unread = connector.getUnreadCountForContactKey(keyHex);
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getMessages(widget.contact);
|
||||
Message? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadAnchor(
|
||||
connector.getMessages(widget.contact),
|
||||
unread,
|
||||
);
|
||||
if (unread > 0) {
|
||||
anchor = _findOldestUnreadAnchor(messages, unread);
|
||||
}
|
||||
setState(() {
|
||||
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
|
||||
if (anchor != null && settings.jumpToOldestUnread) {
|
||||
_pendingUnreadScrollTarget = anchor;
|
||||
}
|
||||
});
|
||||
connector.setActiveContact(keyHex);
|
||||
_connector = connector;
|
||||
if (anchor != null) {
|
||||
setState(() => _pendingUnreadScrollTarget = anchor);
|
||||
if (anchor != null && settings.jumpToOldestUnread) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final ctx = _unreadScrollKey.currentContext;
|
||||
if (ctx != null) {
|
||||
Scrollable.ensureVisible(
|
||||
ctx,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
alignment: 0.15,
|
||||
);
|
||||
}
|
||||
setState(() => _pendingUnreadScrollTarget = null);
|
||||
_scrollController.jumpToEstimatedOffset(
|
||||
unreadCount: unread,
|
||||
totalMessages: messages.length,
|
||||
onJumped: () async {
|
||||
if (!mounted) return;
|
||||
final ctx = _unreadScrollKey.currentContext;
|
||||
if (ctx != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
ctx,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
alignment: 0.15,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _pendingUnreadScrollTarget = null);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -116,6 +136,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void _clearDividerAtBottom() {
|
||||
if (!_scrollController.showJumpToBottom.value &&
|
||||
_unreadDividerMessageId != null) {
|
||||
setState(() => _unreadDividerMessageId = null);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTextFieldFocusChange() {
|
||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||
_scrollController.handleKeyboardOpen();
|
||||
@@ -137,6 +164,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_connector?.setActiveContact(null);
|
||||
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
|
||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||
_textFieldFocusNode.dispose();
|
||||
_textController.dispose();
|
||||
@@ -479,6 +507,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
senderName: resolvedContact.type == advTypeRoom
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
: contact.name,
|
||||
sourceId: widget.contact.publicKeyHex,
|
||||
isRoomServer: resolvedContact.type == advTypeRoom,
|
||||
textScale: textScale,
|
||||
onTap: () => _openMessagePath(message, contact),
|
||||
@@ -486,10 +515,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
onRetryReaction: (msg, emoji) =>
|
||||
_sendReaction(msg, contact, emoji),
|
||||
);
|
||||
final isUnreadAnchor =
|
||||
_unreadDividerMessageId != null &&
|
||||
message.messageId == _unreadDividerMessageId;
|
||||
final child = isUnreadAnchor
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const UnreadDivider(), bubble],
|
||||
)
|
||||
: bubble;
|
||||
if (identical(message, _pendingUnreadScrollTarget)) {
|
||||
return KeyedSubtree(key: _unreadScrollKey, child: bubble);
|
||||
return KeyedSubtree(key: _unreadScrollKey, child: child);
|
||||
}
|
||||
return bubble;
|
||||
return child;
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -497,6 +535,18 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _markAsUnread(Message message) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final messages = connector.getMessages(widget.contact);
|
||||
var count = 0;
|
||||
var found = false;
|
||||
for (final m in messages) {
|
||||
if (m.messageId == message.messageId) found = true;
|
||||
if (found && !m.isOutgoing && !m.isCli) count++;
|
||||
}
|
||||
connector.setContactUnreadCount(widget.contact.publicKeyHex, count);
|
||||
}
|
||||
|
||||
Widget _buildInputBar(MeshCoreConnector connector) {
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -567,24 +617,35 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
return ByteCountedTextField(
|
||||
maxBytes: maxBytes,
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
onSubmitted: (_) => _sendMessage(connector),
|
||||
encoder:
|
||||
connector.isContactSmazEnabled(
|
||||
widget.contact.publicKeyHex,
|
||||
)
|
||||
? (text) => connector.prepareContactOutboundText(
|
||||
widget.contact,
|
||||
text,
|
||||
)
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
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(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(connector),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -672,7 +733,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
}
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
if (utf8.encode(outgoingText).length > maxBytes) {
|
||||
final outboundText = connector.prepareContactOutboundText(
|
||||
_resolveContact(connector),
|
||||
outgoingText,
|
||||
);
|
||||
if (utf8.encode(outboundText).length > maxBytes) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
|
||||
@@ -1153,8 +1218,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
_formatContactLastMessage(contact.lastMessageAt),
|
||||
@@ -1305,11 +1376,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
void _openChat(BuildContext context, Contact contact) {
|
||||
// Check if this is a repeater
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
||||
connector.markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ChatScreen(contact: contact, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1446,6 +1521,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_copyMessageText(message.text);
|
||||
},
|
||||
),
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
title: Text(context.l10n.chat_markAsUnread),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_markAsUnread(message);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: Text(context.l10n.common_delete),
|
||||
@@ -1553,10 +1637,12 @@ class _MessageBubble extends StatelessWidget {
|
||||
final VoidCallback? onLongPress;
|
||||
final void Function(Message message, String emoji)? onRetryReaction;
|
||||
final double textScale;
|
||||
final String sourceId;
|
||||
|
||||
const _MessageBubble({
|
||||
required this.message,
|
||||
required this.senderName,
|
||||
required this.sourceId,
|
||||
required this.isRoomServer,
|
||||
required this.textScale,
|
||||
this.onTap,
|
||||
@@ -1571,7 +1657,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final gifId = GifHelper.parseGif(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
final poi = parseMarkerText(message.text);
|
||||
final isFailed = message.status == MessageStatus.failed;
|
||||
final bubbleColor = isFailed
|
||||
? colorScheme.errorContainer
|
||||
@@ -1663,6 +1749,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
textColor,
|
||||
metaColor,
|
||||
textScale,
|
||||
senderName,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
@@ -1844,25 +1931,13 @@ class _MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
_PoiInfo? _parsePoiMessage(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(
|
||||
r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$',
|
||||
).firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final lat = double.tryParse(match.group(1) ?? '');
|
||||
final lon = double.tryParse(match.group(2) ?? '');
|
||||
if (lat == null || lon == null) return null;
|
||||
final label = match.group(3) ?? '';
|
||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
||||
}
|
||||
|
||||
Widget _buildPoiMessage(
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
MarkerPayload poi,
|
||||
Color textColor,
|
||||
Color metaColor,
|
||||
double textScale, {
|
||||
double textScale,
|
||||
String senderName, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
return Row(
|
||||
@@ -1872,13 +1947,23 @@ class _MessageBubble extends StatelessWidget {
|
||||
icon: Icon(Icons.location_on_outlined, color: textColor),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
|
||||
final fromName = message.isOutgoing ? selfName : senderName;
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: sourceId,
|
||||
label: poi.label,
|
||||
fromName: fromName,
|
||||
flags: poi.flags,
|
||||
isChannel: false,
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapScreen(
|
||||
highlightPosition: LatLng(poi.lat, poi.lon),
|
||||
highlightPosition: poi.position,
|
||||
highlightLabel: poi.label,
|
||||
highlightMarkerKey: key,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2059,11 +2144,3 @@ class _MessageBubble extends StatelessWidget {
|
||||
return '$hour:$minute';
|
||||
}
|
||||
}
|
||||
|
||||
class _PoiInfo {
|
||||
final double lat;
|
||||
final double lon;
|
||||
final String label;
|
||||
|
||||
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../models/contact_group.dart';
|
||||
import '../services/ui_view_state_service.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
@@ -930,10 +931,17 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
} else if (contact.type == advTypeRoom) {
|
||||
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
||||
} else {
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ChatScreen(contact: contact, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -988,7 +996,11 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
builder: (context) => RoomLoginDialog(
|
||||
room: room,
|
||||
onLogin: (password, isAdmin) {
|
||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
room.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -999,7 +1011,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
password: password,
|
||||
isAdmin: isAdmin,
|
||||
)
|
||||
: ChatScreen(contact: room),
|
||||
: ChatScreen(contact: room, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1122,7 +1134,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return CheckboxListTile(
|
||||
value: isSelected,
|
||||
title: Text(contact.name),
|
||||
subtitle: Text(contact.typeLabel),
|
||||
subtitle: Text(
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setDialogState(() {
|
||||
if (value == true) {
|
||||
@@ -1464,7 +1478,7 @@ class _ContactTile extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
contact.pathLabel,
|
||||
contact.pathLabel(context.l10n),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -56,12 +57,16 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
static const double _maxAntennaFeet = 400.0;
|
||||
static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet;
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final LineOfSightService _lineOfSightService = LineOfSightService();
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
LineOfSightPathResult? _result;
|
||||
LineOfSightObstruction? _selectedObstruction;
|
||||
LineOfSightEndpoint? _start;
|
||||
LineOfSightEndpoint? _end;
|
||||
final List<LineOfSightEndpoint> _customEndpoints = [];
|
||||
@@ -98,10 +103,85 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
_lineOfSightService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(initialCenter, initialZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final topOffset = _showHud
|
||||
? math.min(screenHeight * 0.52 + 24, screenHeight - 220)
|
||||
: 12.0;
|
||||
return Positioned(
|
||||
top: topOffset,
|
||||
left: 12,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runLos() async {
|
||||
final start = _start;
|
||||
final end = _end;
|
||||
@@ -111,6 +191,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
if (start == null || end == null) {
|
||||
setState(() {
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = _errorSelectStartEnd;
|
||||
});
|
||||
return;
|
||||
@@ -142,6 +223,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
}
|
||||
setState(() {
|
||||
_result = result;
|
||||
_selectedObstruction = _defaultObstructionFor(result);
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -156,6 +238,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
}
|
||||
setState(() {
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = context.l10n.losRunFailed(e.toString());
|
||||
});
|
||||
} finally {
|
||||
@@ -184,6 +267,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
void _selectFromMap(LineOfSightEndpoint endpoint) {
|
||||
setState(() {
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = null;
|
||||
if (_start == null || (_start != null && _end != null)) {
|
||||
_start = endpoint;
|
||||
@@ -241,6 +325,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
_start = null;
|
||||
_end = null;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = _errorSelectStartEnd;
|
||||
});
|
||||
}
|
||||
@@ -251,6 +336,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
if (identical(_start, endpoint)) _start = null;
|
||||
if (identical(_end, endpoint)) _end = null;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -318,6 +404,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
? LatLngBounds.fromPoints(mapPoints)
|
||||
: null;
|
||||
final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
if (!_didReceivePositionUpdate) {
|
||||
_showMarkerLabels = initialZoom >= _labelZoomThreshold;
|
||||
}
|
||||
@@ -343,6 +430,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
body: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
@@ -355,7 +443,19 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onLongPress: (_, point) => _addCustomPoint(point),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
@@ -377,9 +477,17 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
if (_result != null && _result!.segments.isNotEmpty)
|
||||
PolylineLayer(polylines: _buildSegmentPolylines(_result!)),
|
||||
MarkerLayer(markers: _buildMarkers(endpoints)),
|
||||
MarkerLayer(
|
||||
markers: _buildMarkers(endpoints, _primaryObstructions()),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
if (_showHud)
|
||||
Positioned(
|
||||
left: 12,
|
||||
@@ -445,6 +553,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
);
|
||||
final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz;
|
||||
final kFactorUsed = segment?.usedKFactor;
|
||||
final obstructions =
|
||||
segment?.obstructions ?? const <LineOfSightObstruction>[];
|
||||
final endpoints = _visibleEndpoints();
|
||||
final distanceUnit = isImperial ? 'mi' : 'km';
|
||||
final heightUnit = isImperial ? 'ft' : 'm';
|
||||
@@ -463,31 +573,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (segment != null)
|
||||
SizedBox(
|
||||
height: 160,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _LosProfilePainter(
|
||||
samples: segment.samples,
|
||||
distanceUnit: distanceUnit,
|
||||
heightUnit: heightUnit,
|
||||
badgeTextStyle:
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
) ??
|
||||
const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
terrainLabel: context.l10n.losLegendTerrain,
|
||||
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||
),
|
||||
),
|
||||
)
|
||||
_buildProfileView(segment, distanceUnit, heightUnit, isImperial)
|
||||
else
|
||||
SizedBox(
|
||||
height: 44,
|
||||
@@ -519,6 +605,96 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (obstructions.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.losBlockedSpotsTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.losBlockedSpotsHint,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final obstruction in obstructions)
|
||||
ChoiceChip(
|
||||
label: Text(
|
||||
_obstructionChipLabel(obstruction, isImperial),
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
selected:
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex,
|
||||
onSelected: (_) => _selectObstruction(obstruction),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_selectedObstruction != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.deepOrangeAccent.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.losSelectedObstructionTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.losSelectedObstructionDetails(
|
||||
_formatHeightValue(
|
||||
_selectedObstruction!.obstructionMeters,
|
||||
isImperial,
|
||||
),
|
||||
heightUnit,
|
||||
_formatDistanceValue(
|
||||
_selectedObstruction!.distanceMeters,
|
||||
isImperial,
|
||||
),
|
||||
distanceUnit,
|
||||
_formatDistanceValue(
|
||||
segment!.totalDistanceMeters -
|
||||
_selectedObstruction!.distanceMeters,
|
||||
isImperial,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_selectedObstruction!.point.latitude.toStringAsFixed(5)}, '
|
||||
'${_selectedObstruction!.point.longitude.toStringAsFixed(5)}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
if (displayFrequencyMHz != null)
|
||||
Padding(
|
||||
@@ -605,6 +781,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
_showDisplayNodes = value;
|
||||
_sanitizeSelection();
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -655,6 +832,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
setState(() {
|
||||
_start = value;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
if (_start != null && _end != null) {
|
||||
_runLos();
|
||||
@@ -670,6 +848,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
setState(() {
|
||||
_end = value;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
if (_start != null && _end != null) {
|
||||
_runLos();
|
||||
@@ -769,6 +948,179 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
return _result!.segments.first.result;
|
||||
}
|
||||
|
||||
List<LineOfSightObstruction> _primaryObstructions() {
|
||||
return _primarySegmentResult()?.obstructions ?? const [];
|
||||
}
|
||||
|
||||
LineOfSightObstruction? _defaultObstructionFor(
|
||||
LineOfSightPathResult? result,
|
||||
) {
|
||||
if (result == null || result.segments.isEmpty) return null;
|
||||
final obstructions = result.segments.first.result.obstructions;
|
||||
if (obstructions.isEmpty) return null;
|
||||
return obstructions.reduce(
|
||||
(current, next) =>
|
||||
next.obstructionMeters > current.obstructionMeters ? next : current,
|
||||
);
|
||||
}
|
||||
|
||||
void _selectObstruction(LineOfSightObstruction obstruction) {
|
||||
setState(() {
|
||||
_selectedObstruction = obstruction;
|
||||
});
|
||||
}
|
||||
|
||||
String _formatDistanceValue(double meters, bool isImperial) {
|
||||
final value = isImperial ? (meters / 1000.0) * _kmToMiles : meters / 1000.0;
|
||||
return value.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
String _formatHeightValue(double meters, bool isImperial) {
|
||||
final value = isImperial ? meters * _metersToFeet : meters;
|
||||
return value.toStringAsFixed(1);
|
||||
}
|
||||
|
||||
String _obstructionChipLabel(
|
||||
LineOfSightObstruction obstruction,
|
||||
bool isImperial,
|
||||
) {
|
||||
final distanceUnit = isImperial ? 'mi' : 'km';
|
||||
final heightUnit = isImperial ? 'ft' : 'm';
|
||||
return context.l10n.losBlockedSpotChip(
|
||||
_formatDistanceValue(obstruction.distanceMeters, isImperial),
|
||||
distanceUnit,
|
||||
_formatHeightValue(obstruction.obstructionMeters, isImperial),
|
||||
heightUnit,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileView(
|
||||
LineOfSightResult segment,
|
||||
String distanceUnit,
|
||||
String heightUnit,
|
||||
bool isImperial,
|
||||
) {
|
||||
if (segment.samples.length < 2) {
|
||||
return SizedBox(
|
||||
height: 160,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _LosProfilePainter(
|
||||
samples: segment.samples,
|
||||
distanceUnit: distanceUnit,
|
||||
heightUnit: heightUnit,
|
||||
badgeTextStyle:
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
) ??
|
||||
const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
terrainLabel: context.l10n.losLegendTerrain,
|
||||
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||
selectedSampleIndex: _selectedObstruction?.sampleIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 160,
|
||||
width: double.infinity,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final size = Size(constraints.maxWidth, 160);
|
||||
final geometry = _LosProfileGeometry(
|
||||
samples: segment.samples,
|
||||
size: size,
|
||||
);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _LosProfilePainter(
|
||||
samples: segment.samples,
|
||||
distanceUnit: distanceUnit,
|
||||
heightUnit: heightUnit,
|
||||
badgeTextStyle:
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
) ??
|
||||
const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
terrainLabel: context.l10n.losLegendTerrain,
|
||||
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||
selectedSampleIndex: _selectedObstruction?.sampleIndex,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final obstruction in segment.obstructions)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final sample = segment.samples[obstruction.sampleIndex];
|
||||
final position = geometry.mapPoint(
|
||||
sample.distanceMeters,
|
||||
sample.terrainMeters,
|
||||
);
|
||||
final isSelected =
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex;
|
||||
final markerSize = isSelected ? 18.0 : 14.0;
|
||||
final left = (position.dx - markerSize / 2)
|
||||
.clamp(0.0, math.max(0.0, size.width - markerSize))
|
||||
.toDouble();
|
||||
final top = (position.dy - markerSize / 2)
|
||||
.clamp(0.0, math.max(0.0, size.height - markerSize))
|
||||
.toDouble();
|
||||
return Positioned(
|
||||
left: left,
|
||||
top: top,
|
||||
child: Tooltip(
|
||||
message: _obstructionChipLabel(obstruction, isImperial),
|
||||
child: GestureDetector(
|
||||
onTap: () => _selectObstruction(obstruction),
|
||||
child: Container(
|
||||
width: markerSize,
|
||||
height: markerSize,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Colors.amberAccent
|
||||
: Colors.deepOrangeAccent,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: Colors.black87,
|
||||
width: isSelected ? 2 : 1.5,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black45, blurRadius: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _profileStats(LineOfSightResult result, bool isImperial) {
|
||||
final distance = isImperial
|
||||
? (result.totalDistanceMeters / 1000.0) * _kmToMiles
|
||||
@@ -820,8 +1172,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
return polylines;
|
||||
}
|
||||
|
||||
List<Marker> _buildMarkers(List<LineOfSightEndpoint> endpoints) {
|
||||
List<Marker> _buildMarkers(
|
||||
List<LineOfSightEndpoint> endpoints,
|
||||
List<LineOfSightObstruction> obstructions,
|
||||
) {
|
||||
return [
|
||||
for (final obstruction in obstructions)
|
||||
Marker(
|
||||
point: obstruction.point,
|
||||
width: 52,
|
||||
height: 52,
|
||||
child: GestureDetector(
|
||||
onTap: () => _selectObstruction(obstruction),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width:
|
||||
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
|
||||
? 36
|
||||
: 24,
|
||||
height:
|
||||
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
|
||||
? 36
|
||||
: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.transparent,
|
||||
border: Border.all(
|
||||
color:
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex
|
||||
? Colors.amberAccent
|
||||
: Colors.deepOrangeAccent,
|
||||
width:
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex
|
||||
? 4
|
||||
: 3,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final endpoint in endpoints)
|
||||
Marker(
|
||||
point: endpoint.point,
|
||||
@@ -1010,6 +1405,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LosProfileGeometry {
|
||||
static const horizontalPadding = 12.0;
|
||||
static const verticalPadding = 12.0;
|
||||
|
||||
final List<LineOfSightSample> samples;
|
||||
final Size size;
|
||||
late final double minY = samples
|
||||
.map(
|
||||
(s) => math.min(
|
||||
math.min(s.terrainMeters, s.lineHeightMeters),
|
||||
s.refractedHeightMeters,
|
||||
),
|
||||
)
|
||||
.reduce(math.min);
|
||||
late final double maxY = samples
|
||||
.map(
|
||||
(s) => math.max(
|
||||
math.max(s.terrainMeters, s.lineHeightMeters),
|
||||
s.refractedHeightMeters,
|
||||
),
|
||||
)
|
||||
.reduce(math.max);
|
||||
late final double ySpan = math.max(1.0, maxY - minY);
|
||||
late final double maxDist = math.max(1.0, samples.last.distanceMeters);
|
||||
late final double chartWidth = math.max(
|
||||
1.0,
|
||||
size.width - horizontalPadding * 2,
|
||||
);
|
||||
late final double chartHeight = math.max(
|
||||
1.0,
|
||||
size.height - verticalPadding * 2,
|
||||
);
|
||||
|
||||
_LosProfileGeometry({required this.samples, required this.size});
|
||||
|
||||
Offset mapPoint(double distanceMeters, double elevationMeters) {
|
||||
final px = horizontalPadding + (distanceMeters / maxDist) * chartWidth;
|
||||
final py =
|
||||
size.height -
|
||||
verticalPadding -
|
||||
((elevationMeters - minY) / ySpan) * chartHeight;
|
||||
return Offset(px, py);
|
||||
}
|
||||
}
|
||||
|
||||
class _LosProfilePainter extends CustomPainter {
|
||||
final List<LineOfSightSample> samples;
|
||||
final String distanceUnit;
|
||||
@@ -1018,6 +1458,7 @@ class _LosProfilePainter extends CustomPainter {
|
||||
final String terrainLabel;
|
||||
final String losBeamLabel;
|
||||
final String radioHorizonLabel;
|
||||
final int? selectedSampleIndex;
|
||||
|
||||
const _LosProfilePainter({
|
||||
required this.samples,
|
||||
@@ -1027,6 +1468,7 @@ class _LosProfilePainter extends CustomPainter {
|
||||
required this.terrainLabel,
|
||||
required this.losBeamLabel,
|
||||
required this.radioHorizonLabel,
|
||||
this.selectedSampleIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1212,6 +1654,32 @@ class _LosProfilePainter extends CustomPainter {
|
||||
..color = horizonFillColor
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
|
||||
if (selectedSampleIndex != null &&
|
||||
selectedSampleIndex! >= 0 &&
|
||||
selectedSampleIndex! < samples.length) {
|
||||
final selectedSample = samples[selectedSampleIndex!];
|
||||
final selectedPoint = mapPoint(
|
||||
selectedSample.distanceMeters,
|
||||
selectedSample.terrainMeters,
|
||||
);
|
||||
canvas.drawLine(
|
||||
Offset(selectedPoint.dx, verticalPadding),
|
||||
Offset(selectedPoint.dx, size.height - verticalPadding),
|
||||
Paint()
|
||||
..color = Colors.amberAccent.withValues(alpha: 0.7)
|
||||
..strokeWidth = 1.5,
|
||||
);
|
||||
canvas.drawCircle(selectedPoint, 7, Paint()..color = Colors.amberAccent);
|
||||
canvas.drawCircle(
|
||||
selectedPoint,
|
||||
8.5,
|
||||
Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1222,7 +1690,8 @@ class _LosProfilePainter extends CustomPainter {
|
||||
oldDelegate.badgeTextStyle != badgeTextStyle ||
|
||||
oldDelegate.terrainLabel != terrainLabel ||
|
||||
oldDelegate.losBeamLabel != losBeamLabel ||
|
||||
oldDelegate.radioHorizonLabel != radioHorizonLabel;
|
||||
oldDelegate.radioHorizonLabel != radioHorizonLabel ||
|
||||
oldDelegate.selectedSampleIndex != selectedSampleIndex;
|
||||
}
|
||||
|
||||
void _drawUnitBadge(Canvas canvas, Size size) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -18,6 +19,9 @@ class MapCacheScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
LatLngBounds? _selectedBounds;
|
||||
@@ -43,6 +47,61 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView() {
|
||||
final bounds = _selectedBounds;
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(const LatLng(0, 0), 2.0);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls() {
|
||||
return Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final bounds = MapTileCacheService.boundsFromJson(settings.mapCacheBounds);
|
||||
@@ -222,6 +281,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final selectedBounds = _selectedBounds;
|
||||
final l10n = context.l10n;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final progressValue = _estimatedTiles == 0
|
||||
? 0.0
|
||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||
@@ -238,11 +298,24 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: const MapOptions(
|
||||
initialCenter: LatLng(0, 0),
|
||||
options: MapOptions(
|
||||
initialCenter: const LatLng(0, 0),
|
||||
initialZoom: 2.0,
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -265,6 +338,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop) _buildDesktopMapControls(),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
|
||||
+259
-62
@@ -1,6 +1,5 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -16,6 +15,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../services/map_marker_service.dart';
|
||||
@@ -37,6 +37,7 @@ import 'line_of_sight_map_screen.dart';
|
||||
class MapScreen extends StatefulWidget {
|
||||
final LatLng? highlightPosition;
|
||||
final String? highlightLabel;
|
||||
final String? highlightMarkerKey;
|
||||
final double highlightZoom;
|
||||
final bool hideBackButton;
|
||||
|
||||
@@ -44,6 +45,7 @@ class MapScreen extends StatefulWidget {
|
||||
super.key,
|
||||
this.highlightPosition,
|
||||
this.highlightLabel,
|
||||
this.highlightMarkerKey,
|
||||
this.highlightZoom = 15.0,
|
||||
this.hideBackButton = false,
|
||||
});
|
||||
@@ -55,6 +57,8 @@ class MapScreen extends StatefulWidget {
|
||||
class _MapScreenState extends State<MapScreen> {
|
||||
// Zoom level at which node labels start to appear
|
||||
static const double _labelZoomThreshold = 14.0;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
@@ -94,6 +98,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_removedMarkerIds = ids;
|
||||
_removedMarkersLoaded = true;
|
||||
});
|
||||
// If this screen was opened to highlight a marker, and that marker
|
||||
// was previously removed, re-enable it now that we've loaded the saved
|
||||
// removed IDs.
|
||||
if (widget.highlightMarkerKey != null &&
|
||||
_removedMarkerIds.contains(widget.highlightMarkerKey)) {
|
||||
final updated = Set<String>.from(_removedMarkerIds);
|
||||
updated.remove(widget.highlightMarkerKey);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_removedMarkerIds = updated;
|
||||
});
|
||||
await _markerService.saveRemovedIds(updated);
|
||||
}
|
||||
}
|
||||
|
||||
bool _checkLocationPlausibility(double lat, double lon) {
|
||||
@@ -134,11 +151,62 @@ class _MapScreenState extends State<MapScreen> {
|
||||
return zoom.clamp(4.0, 15.0);
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls(
|
||||
BuildContext context, {
|
||||
required LatLng center,
|
||||
required double zoom,
|
||||
required bool hasPathSelector,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: 16,
|
||||
top: hasPathSelector ? null : 16,
|
||||
bottom: hasPathSelector ? 16 : null,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _mapController.move(center, zoom),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
||||
builder: (context, connector, settingsService, pathHistory, child) {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final settings = settingsService.settings;
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
@@ -229,6 +297,24 @@ class _MapScreenState extends State<MapScreen> {
|
||||
: <Polyline>[],
|
||||
);
|
||||
|
||||
// Collect polylines for shared markers' history with dashed lines
|
||||
final List<Polyline> sharedMarkerPolylines = [];
|
||||
for (final marker in sharedMarkers) {
|
||||
if (marker.history.isNotEmpty) {
|
||||
final points = List<LatLng>.from(marker.history);
|
||||
points.add(marker.position);
|
||||
sharedMarkerPolylines.add(
|
||||
Polyline(
|
||||
points: points,
|
||||
color: marker.isChannel
|
||||
? (marker.isPublicChannel ? Colors.orange : Colors.purple)
|
||||
: Colors.blue,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate center and zoom of all nodes, or default to (0, 0)
|
||||
LatLng center = const LatLng(0, 0);
|
||||
double initialZoom = 10.0;
|
||||
@@ -417,10 +503,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
options: MapOptions(
|
||||
initialCenter: center,
|
||||
initialZoom: initialZoom,
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onTap: (_, latLng) {
|
||||
if (_isSelectingPoi) {
|
||||
@@ -475,6 +571,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
||||
PolylineLayer(polylines: _polylines),
|
||||
if (sharedMarkerPolylines.isNotEmpty)
|
||||
PolylineLayer(polylines: sharedMarkerPolylines),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
if (highlightPosition != null)
|
||||
@@ -562,6 +660,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
sharedMarkers.length,
|
||||
guessedLocations.length,
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
context,
|
||||
center: center,
|
||||
zoom: initialZoom,
|
||||
hasPathSelector: _isBuildingPathTrace,
|
||||
),
|
||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||
],
|
||||
),
|
||||
@@ -1239,28 +1344,39 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) {
|
||||
final markers = <_SharedMarker>[];
|
||||
// Build a _SharedMarker per message (history empty), grouped by dedupe key.
|
||||
// Afterwards pick the latest per key and fill its history from older ones.
|
||||
final updatesByKey = <String, List<_SharedMarker>>{};
|
||||
final selfName = connector.selfName ?? 'Me';
|
||||
|
||||
void addUpdate(_SharedMarker update) {
|
||||
(updatesByKey[update.id] ??= <_SharedMarker>[]).add(update);
|
||||
}
|
||||
|
||||
for (final contact in connector.contacts) {
|
||||
final messages = connector.getMessages(contact);
|
||||
for (final message in messages) {
|
||||
final payload = _parseMarkerText(message.text);
|
||||
final payload = parseMarkerText(message.text);
|
||||
if (payload == null) continue;
|
||||
final fromName = message.isOutgoing ? selfName : contact.name;
|
||||
final id = _buildMarkerId(
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: contact.publicKeyHex,
|
||||
timestamp: message.timestamp,
|
||||
text: message.text,
|
||||
label: payload.label,
|
||||
fromName: fromName,
|
||||
flags: payload.flags,
|
||||
isChannel: false,
|
||||
);
|
||||
markers.add(
|
||||
addUpdate(
|
||||
_SharedMarker(
|
||||
id: id,
|
||||
id: key,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
label: payload.label.isEmpty
|
||||
? context.l10n.map_sharedPin
|
||||
: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: fromName,
|
||||
sourceLabel: contact.name,
|
||||
timestamp: message.timestamp,
|
||||
isChannel: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
@@ -1272,23 +1388,28 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final isPublic = _isPublicChannel(channel);
|
||||
final messages = connector.getChannelMessages(channel);
|
||||
for (final message in messages) {
|
||||
final payload = _parseMarkerText(message.text);
|
||||
final payload = parseMarkerText(message.text);
|
||||
if (payload == null) continue;
|
||||
final id = _buildMarkerId(
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: 'channel:${channel.index}',
|
||||
timestamp: message.timestamp,
|
||||
text: message.text,
|
||||
label: payload.label,
|
||||
fromName: message.senderName,
|
||||
flags: payload.flags,
|
||||
isChannel: true,
|
||||
);
|
||||
markers.add(
|
||||
addUpdate(
|
||||
_SharedMarker(
|
||||
id: id,
|
||||
id: key,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
label: payload.label.isEmpty
|
||||
? context.l10n.map_sharedPin
|
||||
: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: message.senderName,
|
||||
sourceLabel: channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name,
|
||||
timestamp: message.timestamp,
|
||||
isChannel: true,
|
||||
isPublicChannel: isPublic,
|
||||
),
|
||||
@@ -1296,38 +1417,27 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
final markers = <_SharedMarker>[];
|
||||
updatesByKey.forEach((_, updates) {
|
||||
updates.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final latest = updates.last;
|
||||
// History: older positions, drop consecutive duplicates at same position.
|
||||
final history = <LatLng>[];
|
||||
for (var i = 0; i < updates.length - 1; i++) {
|
||||
final p = updates[i].position;
|
||||
if (history.isEmpty ||
|
||||
history.last.latitude != p.latitude ||
|
||||
history.last.longitude != p.longitude) {
|
||||
history.add(p);
|
||||
}
|
||||
}
|
||||
markers.add(latest.copyWithHistory(history));
|
||||
});
|
||||
|
||||
markers.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
return markers;
|
||||
}
|
||||
|
||||
_MarkerPayload? _parseMarkerText(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (!trimmed.startsWith('m:')) return null;
|
||||
|
||||
final parts = trimmed.substring(2).split('|');
|
||||
if (parts.isEmpty) return null;
|
||||
final coords = parts[0].split(',');
|
||||
if (coords.length != 2) return null;
|
||||
final lat = double.tryParse(coords[0].trim());
|
||||
final lon = double.tryParse(coords[1].trim());
|
||||
if (lat == null || lon == null) return null;
|
||||
|
||||
final label = parts.length > 1 ? parts[1].trim() : '';
|
||||
final flags = parts.length > 2 ? parts[2].trim() : '';
|
||||
return _MarkerPayload(
|
||||
position: LatLng(lat, lon),
|
||||
label: label.isEmpty ? context.l10n.map_sharedPin : label,
|
||||
flags: flags,
|
||||
);
|
||||
}
|
||||
|
||||
String _buildMarkerId({
|
||||
required String sourceId,
|
||||
required DateTime timestamp,
|
||||
required String text,
|
||||
}) {
|
||||
return '$sourceId|${timestamp.millisecondsSinceEpoch}|$text';
|
||||
}
|
||||
|
||||
Marker _buildSharedMarker(_SharedMarker marker) {
|
||||
final markerColor = marker.isChannel
|
||||
? (marker.isPublicChannel ? Colors.orange : Colors.purple)
|
||||
@@ -1337,7 +1447,15 @@ class _MapScreenState extends State<MapScreen> {
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showMarkerInfo(marker),
|
||||
onTap: () async {
|
||||
if (_removedMarkerIds.contains(marker.id)) {
|
||||
setState(() {
|
||||
_removedMarkerIds.remove(marker.id);
|
||||
});
|
||||
await _markerService.saveRemovedIds(_removedMarkerIds);
|
||||
}
|
||||
_showMarkerInfo(marker);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -1391,11 +1509,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
room: room,
|
||||
// onLogin(password, isAdmin) isAdmin not used for room caht screen
|
||||
onLogin: (password, _) {
|
||||
// Navigate to chat screen after successful login
|
||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
room.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: room)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ChatScreen(contact: room, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -1425,23 +1549,29 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('Type', contact.typeLabel),
|
||||
_buildInfoRow('Path', contact.pathLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
'Location',
|
||||
context.l10n.map_location,
|
||||
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
|
||||
)
|
||||
else if (guessedPosition != null)
|
||||
_buildInfoRow(
|
||||
'Est. Location',
|
||||
context.l10n.map_estLocation,
|
||||
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_lastSeen,
|
||||
_formatLastSeen(contact.lastSeen),
|
||||
),
|
||||
_buildInfoRow('Public Key', contact.publicKeyHex),
|
||||
_buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -1456,11 +1586,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (!contact.isActive) {
|
||||
connector.importDiscoveredContact(contact);
|
||||
}
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(contact: contact),
|
||||
builder: (context) => ChatScreen(
|
||||
contact: contact,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1542,7 +1678,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(marker.label),
|
||||
title: Text(
|
||||
marker.label.isEmpty ? context.l10n.map_sharedPin : marker.label,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1550,7 +1688,11 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_buildInfoRow(context.l10n.map_from, marker.fromName),
|
||||
_buildInfoRow(context.l10n.map_source, marker.sourceLabel),
|
||||
_buildInfoRow(
|
||||
'Location',
|
||||
context.l10n.map_sharedAt,
|
||||
_formatLastSeen(marker.timestamp),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_location,
|
||||
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
if (marker.flags.isNotEmpty)
|
||||
@@ -1715,6 +1857,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
String defaultLabel,
|
||||
) async {
|
||||
final controller = TextEditingController(text: defaultLabel);
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
@@ -2019,7 +2165,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
enabled: settings.mapKeyPrefixEnabled,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.map_publicKeyPrefix,
|
||||
hintText: 'e.g. ab12',
|
||||
hintText: context.l10n.map_publicKeyPrefixHint,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
@@ -2310,18 +2456,50 @@ class _GuessedLocation {
|
||||
});
|
||||
}
|
||||
|
||||
class _MarkerPayload {
|
||||
class MarkerPayload {
|
||||
final LatLng position;
|
||||
final String label;
|
||||
final String flags;
|
||||
|
||||
_MarkerPayload({
|
||||
MarkerPayload({
|
||||
required this.position,
|
||||
required this.label,
|
||||
required this.flags,
|
||||
});
|
||||
}
|
||||
|
||||
/// Parse a shared marker text message of the form
|
||||
/// `m:<lat>,<lon>|<label>|<flags>` and return a [MarkerPayload].
|
||||
MarkerPayload? parseMarkerText(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(
|
||||
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|(.*)',
|
||||
).firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final lat = double.tryParse(match.group(1) ?? '');
|
||||
final lon = double.tryParse(match.group(2) ?? '');
|
||||
if (lat == null || lon == null) return null;
|
||||
final label = (match.group(3) ?? '').trim();
|
||||
final flags = (match.group(4) ?? '').trim();
|
||||
return MarkerPayload(position: LatLng(lat, lon), label: label, flags: flags);
|
||||
}
|
||||
|
||||
/// Build a normalized dedupe key for shared markers.
|
||||
/// Keeps the same algorithm previously present in both chat and map screens.
|
||||
String buildSharedMarkerKey({
|
||||
required String sourceId,
|
||||
required String label,
|
||||
required String fromName,
|
||||
required String flags,
|
||||
required bool isChannel,
|
||||
}) {
|
||||
final normalizedLabel = label.trim().toLowerCase();
|
||||
final normalizedFrom = fromName.trim().toLowerCase();
|
||||
final normalizedFlags = flags.trim().toLowerCase();
|
||||
final scope = isChannel ? 'ch' : 'dm';
|
||||
return '$scope|$sourceId|$normalizedFrom|$normalizedLabel|$normalizedFlags';
|
||||
}
|
||||
|
||||
class _SharedMarker {
|
||||
final String id;
|
||||
final LatLng position;
|
||||
@@ -2329,8 +2507,10 @@ class _SharedMarker {
|
||||
final String flags;
|
||||
final String fromName;
|
||||
final String sourceLabel;
|
||||
final DateTime timestamp;
|
||||
final bool isChannel;
|
||||
final bool isPublicChannel;
|
||||
final List<LatLng> history;
|
||||
|
||||
_SharedMarker({
|
||||
required this.id,
|
||||
@@ -2339,7 +2519,24 @@ class _SharedMarker {
|
||||
required this.flags,
|
||||
required this.fromName,
|
||||
required this.sourceLabel,
|
||||
required this.timestamp,
|
||||
required this.isChannel,
|
||||
required this.isPublicChannel,
|
||||
this.history = const [],
|
||||
});
|
||||
|
||||
_SharedMarker copyWithHistory(List<LatLng> newHistory) {
|
||||
return _SharedMarker(
|
||||
id: id,
|
||||
position: position,
|
||||
label: label,
|
||||
flags: flags,
|
||||
fromName: fromName,
|
||||
sourceLabel: sourceLabel,
|
||||
timestamp: timestamp,
|
||||
isChannel: isChannel,
|
||||
isPublicChannel: isPublicChannel,
|
||||
history: newHistory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,12 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
|
||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Timer? _timeoutTimer;
|
||||
|
||||
@@ -116,11 +119,74 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
_frameSubscription?.cancel();
|
||||
_timeoutTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView() {
|
||||
final bounds = _bounds;
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final center = _initialCenter;
|
||||
if (center != null) {
|
||||
_mapController.move(center, _initialZoom);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls() {
|
||||
return Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List buildPath(Uint8List pathBytes) {
|
||||
Uint8List traceBytes;
|
||||
|
||||
@@ -519,6 +585,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
if (_hasData)
|
||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||
if (_hasData && _isDesktopPlatform(defaultTargetPlatform))
|
||||
_buildDesktopMapControls(),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
@@ -801,10 +869,24 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
MapTileCacheService tileCache,
|
||||
Contact? target,
|
||||
) {
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
initialCenter: _initialCenter!,
|
||||
initialZoom: _initialZoom,
|
||||
initialCameraFit: _bounds == null
|
||||
@@ -814,8 +896,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (shouldShow != _showNodeLabels && mounted) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import 'repeater_status_screen.dart';
|
||||
import 'repeater_cli_screen.dart';
|
||||
@@ -93,7 +94,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
repeater.pathLabel,
|
||||
repeater.pathLabel(context.l10n),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
if (repeater.hasLocation) ...[
|
||||
|
||||
@@ -353,9 +353,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
title: Text("Delete All Paths"),
|
||||
title: Text(l10n.settings_deleteAllPaths),
|
||||
subtitle: Text(
|
||||
"Clear all path data from contacts.",
|
||||
l10n.settings_deleteAllPathsSubtitle,
|
||||
style: TextStyle(color: Colors.red[700]),
|
||||
),
|
||||
onTap: () => connector.deleteAllPaths(),
|
||||
@@ -1023,6 +1023,15 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
},
|
||||
),
|
||||
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>(
|
||||
initialValue: telemetryMode,
|
||||
decoration: InputDecoration(
|
||||
@@ -1064,21 +1073,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());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
|
||||
class BackgroundService {
|
||||
bool _initialized = false;
|
||||
String? Function()? _languageOverrideProvider;
|
||||
|
||||
/// Allows the app to expose its current language override (e.g. from
|
||||
/// AppSettingsService) so the foreground notification matches the app UI
|
||||
/// language instead of only the system locale.
|
||||
void setLanguageOverrideProvider(String? Function()? provider) {
|
||||
_languageOverrideProvider = provider;
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (!PlatformInfo.isAndroid || _initialized) return;
|
||||
@@ -34,13 +45,31 @@ class BackgroundService {
|
||||
}
|
||||
final running = await FlutterForegroundTask.isRunningService;
|
||||
if (running) return;
|
||||
final l10n = await _loadLocalizations();
|
||||
await FlutterForegroundTask.startService(
|
||||
notificationTitle: 'MeshCore running',
|
||||
notificationText: 'Keeping BLE connected',
|
||||
notificationTitle: l10n.background_serviceTitle,
|
||||
notificationText: l10n.background_serviceText,
|
||||
callback: startCallback,
|
||||
);
|
||||
}
|
||||
|
||||
Future<AppLocalizations> _loadLocalizations() async {
|
||||
final supported = AppLocalizations.supportedLocales;
|
||||
final override = _languageOverrideProvider?.call();
|
||||
if (override != null && override.isNotEmpty) {
|
||||
final overrideLocale = Locale(override);
|
||||
final isSupported = supported.any(
|
||||
(l) => l.languageCode == overrideLocale.languageCode,
|
||||
);
|
||||
if (isSupported) {
|
||||
return AppLocalizations.delegate.load(overrideLocale);
|
||||
}
|
||||
}
|
||||
final preferred = WidgetsBinding.instance.platformDispatcher.locales;
|
||||
final match = basicLocaleListResolution(preferred, supported);
|
||||
return AppLocalizations.delegate.load(match);
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
if (!PlatformInfo.isAndroid) return;
|
||||
final running = await FlutterForegroundTask.isRunningService;
|
||||
|
||||
@@ -24,6 +24,26 @@ class LineOfSightSample {
|
||||
});
|
||||
}
|
||||
|
||||
class LineOfSightObstruction {
|
||||
final int sampleIndex;
|
||||
final LatLng point;
|
||||
final double distanceMeters;
|
||||
final double clearanceMeters;
|
||||
final double obstructionMeters;
|
||||
final double terrainMeters;
|
||||
final double lineHeightMeters;
|
||||
|
||||
const LineOfSightObstruction({
|
||||
required this.sampleIndex,
|
||||
required this.point,
|
||||
required this.distanceMeters,
|
||||
required this.clearanceMeters,
|
||||
required this.obstructionMeters,
|
||||
required this.terrainMeters,
|
||||
required this.lineHeightMeters,
|
||||
});
|
||||
}
|
||||
|
||||
class LineOfSightResult {
|
||||
final bool hasData;
|
||||
final bool isClear;
|
||||
@@ -31,6 +51,7 @@ class LineOfSightResult {
|
||||
final double maxObstructionMeters;
|
||||
final double? firstObstructionDistanceMeters;
|
||||
final List<LineOfSightSample> samples;
|
||||
final List<LineOfSightObstruction> obstructions;
|
||||
final String? errorMessage;
|
||||
final double usedKFactor;
|
||||
final double? frequencyMHz;
|
||||
@@ -42,6 +63,7 @@ class LineOfSightResult {
|
||||
required this.maxObstructionMeters,
|
||||
required this.firstObstructionDistanceMeters,
|
||||
required this.samples,
|
||||
required this.obstructions,
|
||||
required this.usedKFactor,
|
||||
this.frequencyMHz,
|
||||
this.errorMessage,
|
||||
@@ -56,7 +78,8 @@ class LineOfSightResult {
|
||||
isClear = false,
|
||||
maxObstructionMeters = 0,
|
||||
firstObstructionDistanceMeters = null,
|
||||
samples = const [];
|
||||
samples = const [],
|
||||
obstructions = const [];
|
||||
}
|
||||
|
||||
class LineOfSightPathSegment {
|
||||
@@ -191,6 +214,7 @@ class LineOfSightService {
|
||||
maxObstructionMeters: 0,
|
||||
firstObstructionDistanceMeters: null,
|
||||
samples: const [],
|
||||
obstructions: const [],
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
@@ -249,7 +273,9 @@ class LineOfSightService {
|
||||
var maxObstructionMeters = 0.0;
|
||||
double? firstObstructionDistanceMeters;
|
||||
final samples = <LineOfSightSample>[];
|
||||
final obstructions = <LineOfSightObstruction>[];
|
||||
var isClear = true;
|
||||
LineOfSightObstruction? clusterWorstObstruction;
|
||||
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
|
||||
@@ -274,6 +300,23 @@ class LineOfSightService {
|
||||
maxObstructionMeters = obstruction;
|
||||
}
|
||||
firstObstructionDistanceMeters ??= distanceFromStart;
|
||||
final candidate = LineOfSightObstruction(
|
||||
sampleIndex: i,
|
||||
point: points[i],
|
||||
distanceMeters: distanceFromStart,
|
||||
clearanceMeters: clearance,
|
||||
obstructionMeters: obstruction,
|
||||
terrainMeters: terrainHeight,
|
||||
lineHeightMeters: lineHeight,
|
||||
);
|
||||
if (clusterWorstObstruction == null ||
|
||||
candidate.obstructionMeters >
|
||||
clusterWorstObstruction.obstructionMeters) {
|
||||
clusterWorstObstruction = candidate;
|
||||
}
|
||||
} else if (clusterWorstObstruction != null) {
|
||||
obstructions.add(clusterWorstObstruction);
|
||||
clusterWorstObstruction = null;
|
||||
}
|
||||
|
||||
samples.add(
|
||||
@@ -286,6 +329,9 @@ class LineOfSightService {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (clusterWorstObstruction != null) {
|
||||
obstructions.add(clusterWorstObstruction);
|
||||
}
|
||||
|
||||
return LineOfSightResult(
|
||||
hasData: true,
|
||||
@@ -294,6 +340,7 @@ class LineOfSightService {
|
||||
maxObstructionMeters: maxObstructionMeters,
|
||||
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
|
||||
samples: samples,
|
||||
obstructions: obstructions,
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
|
||||
@@ -519,12 +519,11 @@ class TranslationService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
String? _heuristicLanguageCode(String text) {
|
||||
if (RegExp(r'[іїєґІЇЄҐ]').hasMatch(text)) {
|
||||
return 'uk';
|
||||
}
|
||||
if (RegExp(r'[а-яёА-ЯЁ]').hasMatch(text)) {
|
||||
return 'ru';
|
||||
final trimmed = text.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
|
||||
return 'ja';
|
||||
}
|
||||
@@ -534,9 +533,55 @@ class TranslationService extends ChangeNotifier {
|
||||
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
|
||||
return 'zh';
|
||||
}
|
||||
// Latin-script languages can't be reliably distinguished by characters
|
||||
// alone — return null so the translator always attempts translation.
|
||||
return null;
|
||||
|
||||
final lower = trimmed.toLowerCase();
|
||||
final patterns = <String, String>{
|
||||
'uk': r'\b(привіт|дякую|будь|ласка|як|де|не|так|це|є|най|ще|може|для)\b',
|
||||
'ru':
|
||||
r'\b(что|это|как|не|да|нет|он|она|они|быть|есть|для|сегодня|если|уже|может)\b',
|
||||
'bg': r'\b(ще|няма|благодаря|моля|това|какво|тук|ние|вие|не|със|за)\b',
|
||||
'de':
|
||||
r'\b(der|die|das|und|ist|nicht|ein|eine|ich|für|mit|auf|zu|auch|als|an|im|am|es|dem|den|sich|von)\b',
|
||||
'en':
|
||||
r'\b(the|and|is|you|for|with|from|not|that|this|have|be|are|was|were|but|can|will|your|what|when|how|they)\b',
|
||||
'es':
|
||||
r'\b(el|la|los|las|es|que|de|en|con|por|para|no|un|una|se|como|su|al|del|está)\b',
|
||||
'fr':
|
||||
r'\b(le|la|les|un|une|et|est|que|qui|pour|dans|pas|avec|sur|ne|vous|il|elle|des|ce|cette|je|tu|nous|vous)\b',
|
||||
'it':
|
||||
r'\b(il|la|lo|un|una|che|di|da|in|per|con|non|si|mi|ti|noi|voi|lui|lei)\b',
|
||||
'pt':
|
||||
r'\b(os|as|que|de|do|da|em|para|com|por|não|uma|um|se|você|também)\b',
|
||||
'nl':
|
||||
r'\b(de|het|een|en|is|niet|dat|wat|je|ik|op|aan|voor|met|als|nog|zijn)\b',
|
||||
'sv':
|
||||
r'\b(och|är|det|att|som|en|på|inte|har|var|men|du|jag|vi|ni|den|detta)\b',
|
||||
'pl':
|
||||
r'\b(na|się|nie|jest|to|że|do|od|dla|czy|tak|ale|ma|jak|on|ona|my)\b',
|
||||
'sk': r'\b(je|na|so|že|do|od|za|si|to|ten|tá|tí|ako|má|nie|som|sa)\b',
|
||||
'sl': r'\b(in|je|na|se|da|za|od|ne|to|ta|so|kako|bo|sem|si)\b',
|
||||
'hu':
|
||||
r'\b(az|és|nem|van|volt|hogy|mit|mire|ki|mi|ez|azért|is|de|ha|te|ő|mi|itt)\b',
|
||||
};
|
||||
|
||||
final scores = <String, int>{};
|
||||
for (final entry in patterns.entries) {
|
||||
scores[entry.key] = RegExp(
|
||||
entry.value,
|
||||
caseSensitive: false,
|
||||
).allMatches(lower).length;
|
||||
}
|
||||
|
||||
final sorted = scores.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
if (sorted.isEmpty || sorted.first.value == 0) {
|
||||
return null;
|
||||
}
|
||||
if (sorted.length > 1 && sorted.first.value == sorted[1].value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sorted.first.key;
|
||||
}
|
||||
|
||||
String _languageLabel(String code) {
|
||||
|
||||
@@ -43,6 +43,7 @@ class ContactDiscoveryStore {
|
||||
'latitude': contact.latitude,
|
||||
'longitude': contact.longitude,
|
||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||
'lastModified': contact.lastModified?.millisecondsSinceEpoch,
|
||||
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||
'rawPacket': contact.rawPacket != null
|
||||
? base64Encode(contact.rawPacket!)
|
||||
@@ -53,6 +54,7 @@ class ContactDiscoveryStore {
|
||||
Contact _fromJson(Map<String, dynamic> json) {
|
||||
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
||||
final lastMessageMs = json['lastMessageAt'] as int?;
|
||||
final lastModifiedMs = json['lastModified'] as int?;
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
@@ -71,6 +73,9 @@ class ContactDiscoveryStore {
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||
lastModified: lastModifiedMs == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(lastModifiedMs),
|
||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastMessageMs ?? lastSeenMs,
|
||||
),
|
||||
|
||||
@@ -75,6 +75,7 @@ class ContactStore {
|
||||
'latitude': contact.latitude,
|
||||
'longitude': contact.longitude,
|
||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||
'lastModified': contact.lastModified?.millisecondsSinceEpoch,
|
||||
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||
'isActive': contact.isActive,
|
||||
'rawPacket': contact.rawPacket != null
|
||||
@@ -86,6 +87,7 @@ class ContactStore {
|
||||
Contact _fromJson(Map<String, dynamic> json) {
|
||||
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
||||
final lastMessageMs = json['lastMessageAt'] as int?;
|
||||
final lastModifiedMs = json['lastModified'] as int?;
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
@@ -104,6 +106,9 @@ class ContactStore {
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||
lastModified: lastModifiedMs == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(lastModifiedMs),
|
||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastMessageMs ?? lastSeenMs,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// MeshCore redesign palette — warm field-journal dark theme with
|
||||
/// phosphor-green signal accents. Mirrors values from the redesign spec.
|
||||
class MeshPalette {
|
||||
MeshPalette._();
|
||||
|
||||
// Surfaces (warm near-black, olive undertone)
|
||||
static const bg = Color(0xFF0F1412);
|
||||
static const bg1 = Color(0xFF161C19);
|
||||
static const bg2 = Color(0xFF1D2521);
|
||||
static const bg3 = Color(0xFF28322D);
|
||||
static const bg4 = Color(0xFF34403A);
|
||||
|
||||
// Lines
|
||||
static const line = Color(0xFF232C28);
|
||||
static const line2 = Color(0xFF34403A);
|
||||
static const line3 = Color(0xFF48564F);
|
||||
|
||||
// Ink
|
||||
static const ink = Color(0xFFEFF3E8);
|
||||
static const ink2 = Color(0xFFBAC4B5);
|
||||
static const ink3 = Color(0xFF7C8B82);
|
||||
static const ink4 = Color(0xFF55635B);
|
||||
|
||||
// Signal (phosphor)
|
||||
static const signal = Color(0xFF7BEFA8);
|
||||
static const signalDim = Color(0xFF4DC580);
|
||||
static const signalBg = Color(0x177BEFA8); // ~9% alpha
|
||||
static const signalLine = Color(0x427BEFA8); // ~26%
|
||||
static const signalGlow = Color(0x597BEFA8); // ~35%
|
||||
|
||||
// Warn (ember)
|
||||
static const warn = Color(0xFFFFA552);
|
||||
static const warnDim = Color(0xFFC27E3C);
|
||||
static const warnBg = Color(0x1CFFA552);
|
||||
static const warnLine = Color(0x4DFFA552);
|
||||
|
||||
// Alert (coral)
|
||||
static const alert = Color(0xFFFF6A5C);
|
||||
static const alertBg = Color(0x1CFF6A5C);
|
||||
static const alertLine = Color(0x52FF6A5C);
|
||||
|
||||
// Blue (dusk sky)
|
||||
static const blue = Color(0xFF7FCBF5);
|
||||
static const blueBg = Color(0x1C7FCBF5);
|
||||
static const blueLine = Color(0x477FCBF5);
|
||||
|
||||
// Magenta
|
||||
static const magenta = Color(0xFFDE7FDB);
|
||||
static const magentaBg = Color(0x1CDE7FDB);
|
||||
static const magentaLine = Color(0x47DE7FDB);
|
||||
|
||||
// Me bubble (mossy)
|
||||
static const me = Color(0xFF1E3527);
|
||||
static const meBorder = Color(0xFF2D5039);
|
||||
static const meInk = Color(0xFFDEF0DC);
|
||||
|
||||
// ── Light variant (used when user explicitly picks light theme)
|
||||
static const lightBg = Color(0xFFF5F3EC);
|
||||
static const lightBg1 = Color(0xFFECE9DF);
|
||||
static const lightBg2 = Color(0xFFE2DED2);
|
||||
static const lightLine = Color(0xFFCAC5B4);
|
||||
static const lightInk = Color(0xFF0F1410);
|
||||
static const lightInk2 = Color(0xFF3D463E);
|
||||
static const lightInk3 = Color(0xFF6A756D);
|
||||
static const lightSignal = Color(0xFF1A7A44);
|
||||
}
|
||||
|
||||
/// Named font stacks — Flutter falls back to system fonts when the named
|
||||
/// family isn't installed, keeping things working without bundled assets.
|
||||
class MeshFonts {
|
||||
MeshFonts._();
|
||||
|
||||
static const sans = 'Inter';
|
||||
static const mono = 'JetBrains Mono';
|
||||
static const display = 'Instrument Serif';
|
||||
|
||||
static const List<String> sansFallback = [
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'Roboto',
|
||||
'Noto Sans',
|
||||
'sans-serif',
|
||||
];
|
||||
static const List<String> monoFallback = [
|
||||
'SF Mono',
|
||||
'Menlo',
|
||||
'Consolas',
|
||||
'Roboto Mono',
|
||||
'monospace',
|
||||
];
|
||||
static const List<String> displayFallback = [
|
||||
'Cormorant Garamond',
|
||||
'Georgia',
|
||||
'Times New Roman',
|
||||
'serif',
|
||||
];
|
||||
}
|
||||
|
||||
/// Radii used consistently across the app.
|
||||
class MeshRadii {
|
||||
MeshRadii._();
|
||||
static const xs = 6.0;
|
||||
static const sm = 10.0;
|
||||
static const md = 14.0;
|
||||
static const lg = 18.0;
|
||||
static const xl = 24.0;
|
||||
static const pill = 999.0;
|
||||
}
|
||||
|
||||
/// Shared helpers exposed via [MeshTheme.of].
|
||||
class MeshTheme {
|
||||
MeshTheme._();
|
||||
|
||||
static ThemeData dark() {
|
||||
const scheme = ColorScheme.dark(
|
||||
primary: MeshPalette.signal,
|
||||
onPrimary: Color(0xFF0A1810),
|
||||
primaryContainer: MeshPalette.signalBg,
|
||||
onPrimaryContainer: MeshPalette.signal,
|
||||
secondary: MeshPalette.blue,
|
||||
onSecondary: Color(0xFF0A1520),
|
||||
tertiary: MeshPalette.magenta,
|
||||
onTertiary: Color(0xFF201020),
|
||||
error: MeshPalette.alert,
|
||||
onError: Color(0xFF1A0A08),
|
||||
errorContainer: MeshPalette.alertBg,
|
||||
onErrorContainer: MeshPalette.alert,
|
||||
surface: MeshPalette.bg,
|
||||
onSurface: MeshPalette.ink,
|
||||
surfaceContainerLowest: MeshPalette.bg,
|
||||
surfaceContainerLow: MeshPalette.bg1,
|
||||
surfaceContainer: MeshPalette.bg1,
|
||||
surfaceContainerHigh: MeshPalette.bg2,
|
||||
surfaceContainerHighest: MeshPalette.bg3,
|
||||
onSurfaceVariant: MeshPalette.ink2,
|
||||
outline: MeshPalette.line2,
|
||||
outlineVariant: MeshPalette.line,
|
||||
shadow: Colors.black,
|
||||
scrim: Colors.black54,
|
||||
inverseSurface: MeshPalette.ink,
|
||||
onInverseSurface: MeshPalette.bg,
|
||||
inversePrimary: MeshPalette.signalDim,
|
||||
);
|
||||
return _build(scheme, Brightness.dark);
|
||||
}
|
||||
|
||||
static ThemeData light() {
|
||||
const scheme = ColorScheme.light(
|
||||
primary: MeshPalette.lightSignal,
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: Color(0xFFD4E8D8),
|
||||
onPrimaryContainer: MeshPalette.lightSignal,
|
||||
secondary: Color(0xFF2F6EA8),
|
||||
onSecondary: Colors.white,
|
||||
tertiary: Color(0xFF8C4A8A),
|
||||
onTertiary: Colors.white,
|
||||
error: Color(0xFFB53D2F),
|
||||
onError: Colors.white,
|
||||
surface: MeshPalette.lightBg,
|
||||
onSurface: MeshPalette.lightInk,
|
||||
surfaceContainerLowest: MeshPalette.lightBg,
|
||||
surfaceContainerLow: MeshPalette.lightBg1,
|
||||
surfaceContainer: MeshPalette.lightBg1,
|
||||
surfaceContainerHigh: MeshPalette.lightBg2,
|
||||
surfaceContainerHighest: Color(0xFFD5D0C0),
|
||||
onSurfaceVariant: MeshPalette.lightInk2,
|
||||
outline: MeshPalette.lightLine,
|
||||
outlineVariant: Color(0xFFDBD6C6),
|
||||
);
|
||||
return _build(scheme, Brightness.light);
|
||||
}
|
||||
|
||||
static ThemeData _build(ColorScheme scheme, Brightness brightness) {
|
||||
final baseText =
|
||||
Typography.material2021(
|
||||
platform: TargetPlatform.android,
|
||||
colorScheme: scheme,
|
||||
).black.apply(
|
||||
bodyColor: scheme.onSurface,
|
||||
displayColor: scheme.onSurface,
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: brightness,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
canvasColor: scheme.surface,
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
textTheme: baseText,
|
||||
dividerColor: scheme.outlineVariant,
|
||||
dividerTheme: DividerThemeData(
|
||||
color: scheme.outlineVariant,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: scheme.surface,
|
||||
foregroundColor: scheme.onSurface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.2,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
iconTheme: IconThemeData(color: scheme.onSurface),
|
||||
shape: Border(
|
||||
bottom: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||
),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
side: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
iconColor: scheme.onSurfaceVariant,
|
||||
textColor: scheme.onSurface,
|
||||
tileColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
hoverElevation: 0,
|
||||
highlightElevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
extendedTextStyle: const TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: scheme.onSurface,
|
||||
side: BorderSide(color: scheme.outline),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: scheme.primary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: scheme.surfaceContainerHigh,
|
||||
hintStyle: TextStyle(color: scheme.onSurfaceVariant),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.outline),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
side: BorderSide(color: scheme.outlineVariant),
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
indicatorColor: scheme.primary.withValues(alpha: 0.14),
|
||||
indicatorShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.mono,
|
||||
fontFamilyFallback: MeshFonts.monoFallback,
|
||||
fontSize: 10,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
letterSpacing: 0.1,
|
||||
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||
);
|
||||
}),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return IconThemeData(
|
||||
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||
size: 22,
|
||||
);
|
||||
}),
|
||||
),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
modalBackgroundColor: scheme.surfaceContainerLow,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(MeshRadii.lg),
|
||||
),
|
||||
),
|
||||
),
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.lg),
|
||||
),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: scheme.surfaceContainerHigh,
|
||||
contentTextStyle: TextStyle(color: scheme.onSurface),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
),
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
color: scheme.surfaceContainerHigh,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
),
|
||||
iconTheme: IconThemeData(color: scheme.onSurfaceVariant, size: 22),
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
);
|
||||
}
|
||||
|
||||
/// Mono text style — sizes default to the body size Inter is using.
|
||||
static TextStyle mono({
|
||||
double? fontSize,
|
||||
FontWeight? fontWeight,
|
||||
Color? color,
|
||||
double? letterSpacing,
|
||||
}) {
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.mono,
|
||||
fontFamilyFallback: MeshFonts.monoFallback,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
letterSpacing: letterSpacing ?? 0.2,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
);
|
||||
}
|
||||
|
||||
/// Serif display style.
|
||||
static TextStyle display({
|
||||
double? fontSize,
|
||||
FontWeight? fontWeight,
|
||||
Color? color,
|
||||
double? letterSpacing,
|
||||
}) {
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.display,
|
||||
fontFamilyFallback: MeshFonts.displayFallback,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight ?? FontWeight.w400,
|
||||
color: color,
|
||||
letterSpacing: letterSpacing ?? -0.2,
|
||||
);
|
||||
}
|
||||
|
||||
/// Small-caps mono label — used for section accents and chip labels.
|
||||
static TextStyle accentLabel({Color? color, double? fontSize}) {
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.mono,
|
||||
fontFamilyFallback: MeshFonts.monoFallback,
|
||||
fontSize: fontSize ?? 9.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.8,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Color-code an SNR value for consistency across the app.
|
||||
static Color snrColor(num? snr, {required bool blocked}) {
|
||||
if (blocked) return MeshPalette.alert;
|
||||
if (snr == null) return MeshPalette.ink3;
|
||||
if (snr > -5) return MeshPalette.signal;
|
||||
if (snr > -12) return MeshPalette.warn;
|
||||
return MeshPalette.alert;
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,8 @@ class GpxExport {
|
||||
contact.name,
|
||||
contact.latitude!,
|
||||
contact.longitude!,
|
||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||
url,
|
||||
"Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,8 +91,8 @@ class GpxExport {
|
||||
contact.name,
|
||||
contact.latitude!,
|
||||
contact.longitude!,
|
||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||
url,
|
||||
"Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -110,8 +110,8 @@ class GpxExport {
|
||||
contact.name,
|
||||
contact.latitude ?? 0.0,
|
||||
contact.longitude ?? 0.0,
|
||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||
url,
|
||||
"Type: ${contact.typeLabelRaw}\nPublic Key: ${contact.publicKeyHex}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
@@ -147,7 +148,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
context,
|
||||
availableContacts: availableContacts,
|
||||
initialPath: pathForInput.isEmpty ? null : pathForInput,
|
||||
currentPathLabel: currentContact.pathLabel,
|
||||
currentPathLabel: currentContact.pathLabel(l10n),
|
||||
onRefresh: connector.isConnected ? connector.getContacts : null,
|
||||
);
|
||||
|
||||
@@ -236,7 +237,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.path_currentPath(currentContact.pathLabel),
|
||||
l10n.path_currentPath(currentContact.pathLabel(l10n)),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -303,7 +304,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
),
|
||||
isThreeLine: true,
|
||||
subtitle: Text(
|
||||
'${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}\n${path.successCount} ${l10n.chat_successes} • Score: ${path.routeWeight.toStringAsFixed(1)}',
|
||||
'${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}\n${path.successCount} ${l10n.chat_successes} • ${l10n.chat_score}: ${path.routeWeight.toStringAsFixed(1)}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
trailing: Row(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class PathSelectionDialog extends StatefulWidget {
|
||||
@@ -311,7 +312,7 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${contact.typeLabel} • ${contact.publicKeyHex.substring(0, 2)}',
|
||||
'${contact.typeLabel(l10n)} • ${contact.publicKeyHex.substring(0, 2)}',
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
trailing: isSelected
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
/// A reusable QR code display widget for sharing data.
|
||||
///
|
||||
/// Features:
|
||||
@@ -197,7 +199,7 @@ class QrCodeShareDialog extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Done'),
|
||||
child: Text(context.l10n.common_done),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
@@ -467,7 +468,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
repeater.pathLabel,
|
||||
repeater.pathLabel(context.l10n),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
@@ -393,7 +394,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
repeater.pathLabel,
|
||||
repeater.pathLabel(context.l10n),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class UnreadDivider extends StatelessWidget {
|
||||
const UnreadDivider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.primary;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: color)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
context.l10n.chat_newMessages,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- 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`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
flserial:
|
||||
@@ -59,8 +56,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
flserial: 3c161e076dfc73458ec5803e7a9a9d2bb85fadf6
|
||||
@@ -73,7 +68,6 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
|
||||
void main() {
|
||||
// Frame layout per the doc comment on buildUpdateContactPathFrame:
|
||||
// [cmd][pub_key x32][type][flags][path_len][path x64][name x32]
|
||||
// [timestamp x4][Lat? x4, Lon? x4][timestamp? x4]
|
||||
//
|
||||
// Base (mandatory) bytes:
|
||||
// 1 cmd + 32 pubKey + 1 type + 1 flags + 1 pathLen + 64 path
|
||||
// + 32 name + 4 timestamp = 136 bytes
|
||||
const int baseFrameLength = 136;
|
||||
|
||||
final pubKey = Uint8List.fromList(List<int>.generate(32, (i) => i));
|
||||
final path = Uint8List.fromList([0xAA, 0xBB]);
|
||||
|
||||
group('buildUpdateContactPathFrame', () {
|
||||
test('omits lat/lon and timestamp tail when neither is provided', () {
|
||||
final frame = buildUpdateContactPathFrame(
|
||||
pubKey,
|
||||
path,
|
||||
path.length,
|
||||
name: 'Alice',
|
||||
);
|
||||
|
||||
// Should be exactly the base frame, no optional tail.
|
||||
expect(frame.length, baseFrameLength);
|
||||
});
|
||||
|
||||
test('appends only an 8-byte lat/lon tail when location is provided', () {
|
||||
final frame = buildUpdateContactPathFrame(
|
||||
pubKey,
|
||||
path,
|
||||
path.length,
|
||||
lat: 49.123456,
|
||||
lon: -123.123456,
|
||||
);
|
||||
|
||||
expect(frame.length, baseFrameLength + 8);
|
||||
});
|
||||
|
||||
test(
|
||||
'appends 8 bytes lat/lon + 4 bytes timestamp when both are provided',
|
||||
() {
|
||||
final frame = buildUpdateContactPathFrame(
|
||||
pubKey,
|
||||
path,
|
||||
path.length,
|
||||
lat: 49.0,
|
||||
lon: -123.0,
|
||||
lastModified: DateTime.utc(2026, 1, 2, 3, 4, 5),
|
||||
);
|
||||
|
||||
expect(frame.length, baseFrameLength + 8 + 4);
|
||||
},
|
||||
);
|
||||
|
||||
test('zero-fills the lat/lon slots and appends timestamp when only '
|
||||
'lastModified is provided', () {
|
||||
final frame = buildUpdateContactPathFrame(
|
||||
pubKey,
|
||||
path,
|
||||
path.length,
|
||||
lastModified: DateTime.utc(2026, 1, 2, 3, 4, 5),
|
||||
);
|
||||
|
||||
// 8 zero bytes for lat/lon + 4 bytes timestamp
|
||||
expect(frame.length, baseFrameLength + 8 + 4);
|
||||
|
||||
// Verify the lat/lon slot is actually zero — guards against a
|
||||
// regression where the function writes garbage into those bytes.
|
||||
final tailStart = baseFrameLength;
|
||||
for (var i = tailStart; i < tailStart + 8; i++) {
|
||||
expect(frame[i], 0, reason: 'byte $i in lat/lon slot must be 0');
|
||||
}
|
||||
});
|
||||
|
||||
test('encodes positive lat/lon as little-endian fixed-point (×1e6)', () {
|
||||
final frame = buildUpdateContactPathFrame(
|
||||
pubKey,
|
||||
path,
|
||||
path.length,
|
||||
lat: 49.123456,
|
||||
lon: -123.123456,
|
||||
);
|
||||
|
||||
// Latitude is the first 4 bytes of the optional tail.
|
||||
final latBytes = ByteData.sublistView(
|
||||
frame,
|
||||
baseFrameLength,
|
||||
baseFrameLength + 4,
|
||||
);
|
||||
final lonBytes = ByteData.sublistView(
|
||||
frame,
|
||||
baseFrameLength + 4,
|
||||
baseFrameLength + 8,
|
||||
);
|
||||
|
||||
expect(latBytes.getInt32(0, Endian.little), (49.123456 * 1e6).round());
|
||||
expect(lonBytes.getInt32(0, Endian.little), (-123.123456 * 1e6).round());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/l10n/app_localizations.dart';
|
||||
import 'package:meshcore_open/l10n/contact_localization.dart';
|
||||
import 'package:meshcore_open/models/contact.dart';
|
||||
|
||||
Contact _contact({
|
||||
int type = advTypeChat,
|
||||
int pathLength = 0,
|
||||
int? pathOverride,
|
||||
}) {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List.generate(32, (i) => i + 1)),
|
||||
name: 'Node',
|
||||
type: type,
|
||||
pathLength: pathLength,
|
||||
path: Uint8List(0),
|
||||
pathOverride: pathOverride,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late AppLocalizations l10n;
|
||||
|
||||
setUpAll(() async {
|
||||
l10n = await AppLocalizations.delegate.load(const Locale('en'));
|
||||
});
|
||||
|
||||
group('Contact.typeLabel', () {
|
||||
test('chat', () {
|
||||
expect(_contact(type: advTypeChat).typeLabel(l10n), 'Chat');
|
||||
});
|
||||
|
||||
test('repeater', () {
|
||||
expect(_contact(type: advTypeRepeater).typeLabel(l10n), 'Repeater');
|
||||
});
|
||||
|
||||
test('room', () {
|
||||
expect(_contact(type: advTypeRoom).typeLabel(l10n), 'Room');
|
||||
});
|
||||
|
||||
test('sensor', () {
|
||||
expect(_contact(type: advTypeSensor).typeLabel(l10n), 'Sensor');
|
||||
});
|
||||
|
||||
test('unknown type falls back', () {
|
||||
expect(_contact(type: 99).typeLabel(l10n), 'Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
group('Contact.pathLabel (override)', () {
|
||||
test('override < 0 -> flood (forced)', () {
|
||||
expect(_contact(pathOverride: -1).pathLabel(l10n), 'Flood (forced)');
|
||||
});
|
||||
|
||||
test('override == 0 -> direct (forced)', () {
|
||||
expect(_contact(pathOverride: 0).pathLabel(l10n), 'Direct (forced)');
|
||||
});
|
||||
|
||||
test('override > 0 -> hops (forced)', () {
|
||||
expect(_contact(pathOverride: 3).pathLabel(l10n), '3 hops (forced)');
|
||||
});
|
||||
|
||||
test('override takes precedence over pathLength', () {
|
||||
expect(
|
||||
_contact(pathLength: 5, pathOverride: -1).pathLabel(l10n),
|
||||
'Flood (forced)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Contact.pathLabel (auto)', () {
|
||||
test('pathLength < 0 -> flood', () {
|
||||
expect(_contact(pathLength: -1).pathLabel(l10n), 'Flood');
|
||||
});
|
||||
|
||||
test('pathLength == 0 -> direct', () {
|
||||
expect(_contact(pathLength: 0).pathLabel(l10n), 'Direct');
|
||||
});
|
||||
|
||||
test('pathLength == 1 -> singular hop', () {
|
||||
expect(_contact(pathLength: 1).pathLabel(l10n), '1 hop');
|
||||
});
|
||||
|
||||
test('pathLength > 1 -> plural hops', () {
|
||||
expect(_contact(pathLength: 4).pathLabel(l10n), '4 hops');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,7 @@ void main() {
|
||||
expect(result.isClear, isTrue);
|
||||
expect(result.maxObstructionMeters, equals(0));
|
||||
expect(result.firstObstructionDistanceMeters, isNull);
|
||||
expect(result.obstructions, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -44,9 +45,32 @@ void main() {
|
||||
expect(result.isClear, isFalse);
|
||||
expect(result.maxObstructionMeters, greaterThan(0));
|
||||
expect(result.firstObstructionDistanceMeters, isNotNull);
|
||||
expect(result.obstructions, hasLength(1));
|
||||
expect(result.obstructions.single.sampleIndex, equals(10));
|
||||
expect(result.obstructions.single.point, equals(points[10]));
|
||||
},
|
||||
);
|
||||
|
||||
test('computeFromElevations groups contiguous blocked samples', () {
|
||||
final points = makePoints(21);
|
||||
final elevations = List<double>.filled(points.length, 100);
|
||||
elevations[9] = 220;
|
||||
elevations[10] = 320;
|
||||
elevations[11] = 240;
|
||||
|
||||
final result = LineOfSightService.computeFromElevations(
|
||||
points: points,
|
||||
elevations: elevations,
|
||||
startAntennaHeightMeters: 1.5,
|
||||
endAntennaHeightMeters: 1.5,
|
||||
kFactor: 4.0 / 3.0,
|
||||
);
|
||||
|
||||
expect(result.obstructions, hasLength(1));
|
||||
expect(result.obstructions.single.sampleIndex, equals(10));
|
||||
expect(result.obstructions.single.obstructionMeters, greaterThan(0));
|
||||
});
|
||||
|
||||
test('analyzePath summarizes clear and blocked segments', () async {
|
||||
final service = LineOfSightService(
|
||||
elevationDataSource: (points) async {
|
||||
|
||||
+86
-1
@@ -1 +1,86 @@
|
||||
{}
|
||||
{
|
||||
"bg": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"hu": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"sk": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"sl": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"sv": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"chat_markAsUnread",
|
||||
"chat_newMessages"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user