Merge branch 'dev' into update-pacox-multibyte

This commit is contained in:
HDDen
2026-06-12 13:39:09 +03:00
95 changed files with 14166 additions and 6756 deletions
+3
View File
@@ -91,3 +91,6 @@ keystore.properties
# Cloudflare Wrangler
.wrangler
# Claude Code local working dir (worktrees, jobs, settings)
.claude/
+164 -33
View File
@@ -195,6 +195,7 @@ class MeshCoreConnector extends ChangeNotifier {
{}; // contactPubKeyHex -> Set of "targetHash_emoji"
StreamSubscription<List<ScanResult>>? _scanSubscription;
StreamSubscription<bool>? _isScanningSubscription;
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
StreamSubscription<List<int>>? _notifySubscription;
Timer? _notifyListenersTimer;
@@ -542,10 +543,22 @@ class MeshCoreConnector extends ChangeNotifier {
if (messages == null) return;
final removed = messages.remove(message);
if (!removed) return;
_retryService?.untrack(message.messageId);
await _messageStore.saveMessages(contactKeyHex, messages);
notifyListeners();
}
Future<void> resendMessage(Contact contact, Message message) async {
await deleteMessage(message);
await sendMessage(
contact,
message.text,
originalText: message.originalText,
translatedLanguageCode: message.translatedLanguageCode,
translationModelId: message.translationModelId,
);
}
Future<void> _loadMessagesForContact(String contactKeyHex) async {
if (_loadedConversationKeys.contains(contactKeyHex)) return;
_loadedConversationKeys.add(contactKeyHex);
@@ -1391,6 +1404,21 @@ class MeshCoreConnector extends ChangeNotifier {
}) async {
if (_state == MeshCoreConnectionState.scanning) return;
// A BLE scan must never disturb an active (or in-progress) non-BLE
// connection. The connection state enum is shared across transports, so
// entering the `scanning` state while connected over TCP/USB would clobber
// the live `connected` state and later reset it to `disconnected`.
if (_state != MeshCoreConnectionState.disconnected ||
_tcpConnector.isConnected ||
_usbManager.isConnected) {
_appDebugLogService?.warn(
'startScan ignored: not idle (state=$_state, '
'tcp=${_tcpConnector.isConnected}, usb=${_usbManager.isConnected})',
tag: 'BLE Scan',
);
return;
}
_scanResults.clear();
_linuxSystemScanResults.clear();
_setState(MeshCoreConnectionState.scanning);
@@ -1444,20 +1472,40 @@ class MeshCoreConnector extends ChangeNotifier {
});
try {
// Filter by the Nordic UART Service UUID rather than by advertised
// name. All MeshCore-compatible firmware (ESP32 + nRF52) advertises this
// service UUID, so this matches every device regardless of the name it
// chooses to advertise (e.g. community forks like the M5 Cardputer that
// do not use a "MeshCore-" name prefix). This mirrors how the official
// app discovers devices. Note: on Android `withKeywords` cannot be
// combined with any other filter, which is why name keywords are not
// used here.
await FlutterBluePlus.startScan(
withKeywords: MeshCoreUuids.deviceNamePrefixes,
withServices: [Guid(MeshCoreUuids.service)],
webOptionalServices: [Guid(MeshCoreUuids.service)],
timeout: timeout,
androidScanMode: AndroidScanMode.lowLatency,
);
} catch (error) {
_appDebugLogService?.warn('Scan/picker failure: $error', tag: 'BLE Scan');
_setState(MeshCoreConnectionState.disconnected);
await stopScan();
rethrow;
}
await Future.delayed(timeout);
await stopScan();
// Reset our shared state when the native scan ends — whether it was stopped
// by the user (stopScan), by the platform timeout, or by Bluetooth turning
// off. This replaces a blocking `Future.delayed(timeout)` tail that kept
// startScan() pending for the whole timeout and made Stop appear ineffective.
// `isScanning` is a re-emit stream that replays its latest value on listen,
// so skip(1) to ignore that and only react to a genuine transition to false.
await _isScanningSubscription?.cancel();
_isScanningSubscription = FlutterBluePlus.isScanning.skip(1).listen((
scanning,
) {
if (!scanning && _state == MeshCoreConnectionState.scanning) {
unawaited(stopScan());
}
});
}
Future<void> _loadLinuxSystemDevicesForScan() async {
@@ -1465,16 +1513,13 @@ class MeshCoreConnector extends ChangeNotifier {
final systemDevices = await FlutterBluePlus.systemDevices([
Guid(MeshCoreUuids.service),
]);
// systemDevices is already filtered by the NUS service UUID above, so no
// additional name-prefix filtering is applied here. This keeps Linux
// discovery name-agnostic and consistent with the main scan path.
_linuxSystemScanResults
..clear()
..addAll(
systemDevices
.where(
(device) => MeshCoreUuids.deviceNamePrefixes.any(
device.platformName.startsWith,
),
)
.map(
systemDevices.map(
(device) => ScanResult(
device: device,
advertisementData: AdvertisementData(
@@ -1534,9 +1579,17 @@ class MeshCoreConnector extends ChangeNotifier {
}
await _scanSubscription?.cancel();
_scanSubscription = null;
await _isScanningSubscription?.cancel();
_isScanningSubscription = null;
if (_state == MeshCoreConnectionState.scanning) {
_setState(MeshCoreConnectionState.disconnected);
// Restore to `connected` if a non-BLE transport is still live, so a stray
// scan can never tear down the reported connection state. Normally there
// is no live transport here and we fall through to `disconnected`.
final restored = (_tcpConnector.isConnected || _usbManager.isConnected)
? MeshCoreConnectionState.connected
: MeshCoreConnectionState.disconnected;
_setState(restored);
}
}
@@ -1778,6 +1831,17 @@ class MeshCoreConnector extends ChangeNotifier {
activeTransport == MeshCoreTransportType.tcp;
}
/// Fast (non-timeout) connect failures are usually a stale link left over
/// from a previous session and recover on an immediate retry. Timeouts mean
/// the device is likely off or out of range, so retrying would only delay
/// genuine failure feedback.
@visibleForTesting
static bool shouldRetryBleConnectAfterError(String errorText) {
final lowerErrorText = errorText.toLowerCase();
return !lowerErrorText.contains('timed out') &&
!lowerErrorText.contains('timeout');
}
Future<void> connect(
BluetoothDevice device, {
String? displayName,
@@ -1854,7 +1918,7 @@ class MeshCoreConnector extends ChangeNotifier {
.connect(
timeout: connectTimeout,
mtu: null,
license: License.free,
license: License.nonprofit,
)
.timeout(
connectTimeout + const Duration(seconds: 2),
@@ -1946,19 +2010,72 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
} else {
try {
await device.connect(
Future<void> attemptConnect() {
return device.connect(
timeout: connectTimeout,
mtu: null,
license: License.free,
license: License.nonprofit,
);
}
// A previous app session (e.g. killed from the iOS app switcher) can
// leave the OS holding a stale link to the peripheral. Clear it before
// connecting so the fresh attempt doesn't race the stale handle.
if (!PlatformInfo.isWeb && device.isConnected) {
_appDebugLogService?.warn(
'Device reports an existing connection before connect; clearing stale link',
tag: 'BLE Connect',
);
try {
await device.disconnect(queue: false);
} catch (cleanupError) {
_appDebugLogService?.warn(
'Stale-link cleanup disconnect failed (continuing): $cleanupError',
tag: 'BLE Connect',
);
}
}
try {
await attemptConnect();
} catch (error) {
_appDebugLogService?.error(
'device.connect() failure: $error',
tag: 'BLE Connect',
);
if (PlatformInfo.isWeb ||
!shouldRetryBleConnectAfterError(error.toString())) {
rethrow;
}
// Fast (non-timeout) failures are usually a stale connection left by
// a previous session; clean up and retry once before surfacing.
_appDebugLogService?.warn(
'Retrying connect once after clearing possible stale connection',
tag: 'BLE Connect',
);
try {
await device.disconnect(queue: false);
} catch (cleanupError) {
_appDebugLogService?.warn(
'Pre-retry cleanup disconnect failed (continuing): $cleanupError',
tag: 'BLE Connect',
);
}
await Future<void>.delayed(const Duration(milliseconds: 500));
try {
await attemptConnect();
_appDebugLogService?.info(
'Retry connect succeeded after stale-connection cleanup',
tag: 'BLE Connect',
);
} catch (retryError) {
_appDebugLogService?.error(
'device.connect() retry failure: $retryError',
tag: 'BLE Connect',
);
rethrow;
}
}
}
if (PlatformInfo.isLinux) {
@@ -2006,7 +2123,7 @@ class MeshCoreConnector extends ChangeNotifier {
await device.connect(
timeout: const Duration(seconds: 15),
mtu: null,
license: License.free,
license: License.nonprofit,
);
services = await device.discoverServices();
} else {
@@ -3321,6 +3438,7 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildRemoveContactFrame(contact.publicKey));
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
_knownContactKeys.remove(contact.publicKeyHex);
unawaited(updateKnownDiscovered());
unawaited(_persistContacts());
_conversations.remove(contact.publicKeyHex);
_loadedConversationKeys.remove(contact.publicKeyHex);
@@ -3539,12 +3657,10 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> sendCliCommand(String command) async {
if (!isConnected) return;
// CLI commands are sent as UTF-8 text with a special prefix
final commandBytes = utf8.encode(command);
final bytes = Uint8List.fromList([0x01, ...commandBytes, 0x00]);
final selfKey = _selfPublicKey;
if (selfKey == null) return;
_lastSentWasCliCommand = true;
await sendFrame(bytes);
await sendFrame(buildSendCliCommandFrame(selfKey, command));
}
Future<void> setNodeName(String name) async {
@@ -4018,8 +4134,8 @@ class MeshCoreConnector extends ChangeNotifier {
_advertLocPolicy = reader.readByte();
final telemetryFlag = reader.readByte();
_telemetryModeBase = telemetryFlag & 0x03;
_telemetryModeEnv = telemetryFlag >> 2 & 0x03;
_telemetryModeLoc = telemetryFlag >> 4 & 0x03;
_telemetryModeLoc = telemetryFlag >> 2 & 0x03;
_telemetryModeEnv = telemetryFlag >> 4 & 0x03;
_manualAddContacts = reader.readByte() & 0x01 == 0x00;
@@ -4368,7 +4484,14 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'Connector',
);
notifyListeners();
removeContact(contactTmp);
unawaited(
removeContact(contactTmp).catchError(
(e) => appLogger.warn(
'Failed to remove self contact: $e',
tag: 'Connector',
),
),
);
return;
}
final contact = getFromDiscovered(contactTmp);
@@ -5626,6 +5749,9 @@ class MeshCoreConnector extends ChangeNotifier {
}
messages.add(message);
if (messages.length > _messageWindowSize) {
messages.removeRange(0, messages.length - _messageWindowSize);
}
_messageStore.saveMessages(pubKeyHex, messages);
notifyListeners();
}
@@ -6014,20 +6140,24 @@ class MeshCoreConnector extends ChangeNotifier {
bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) {
if (existing.text != incoming.text) return false;
// Self-echo: an outgoing message coming back via a repeater. The send is
// delayed by _waitForRadioQuiet (often 10s+) and propagation can add more,
// so the timestamp gap can easily exceed the cross-peer window.
final selfName = _selfName ?? 'Me';
final isSelfEcho =
existing.isOutgoing &&
!incoming.isOutgoing &&
(incoming.senderName == selfName || existing.senderName == selfName);
final windowMs = isSelfEcho ? 10 * 60 * 1000 : 30000;
final diffMs =
(existing.timestamp.millisecondsSinceEpoch -
incoming.timestamp.millisecondsSinceEpoch)
.abs();
if (diffMs > 30000) return false;
if (diffMs > windowMs) return false;
if (existing.senderName == incoming.senderName) return true;
if (existing.isOutgoing && !incoming.isOutgoing) {
final selfName = _selfName ?? 'Me';
if (incoming.senderName == selfName || existing.senderName == selfName) {
return true;
}
}
if (isSelfEcho) return true;
return false;
}
@@ -6259,6 +6389,7 @@ class MeshCoreConnector extends ChangeNotifier {
@override
void dispose() {
_scanSubscription?.cancel();
_isScanningSubscription?.cancel();
_connectionSubscription?.cancel();
_usbFrameSubscription?.cancel();
_notifySubscription?.cancel();
+13 -2
View File
@@ -224,6 +224,12 @@ const int reqTypeGetTelemetry = 0x03;
const int reqTypeGetAccessList = 0x05;
const int reqTypeGetNeighbors = 0x06;
Uint8List buildTelemetryBinaryPayload() {
// Room servers/repeaters read byte 1 as an inverse telemetry permission mask.
// Zero means "request every telemetry field allowed for this contact".
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
}
// Repeater response codes
const int respServerLoginOk = 0;
@@ -451,8 +457,13 @@ String pubKeyToHex(Uint8List pubKey) {
// Helper to convert hex string to public key
Uint8List hexToPubKey(String hex) {
if (hex.length != pubKeySize * 2) {
throw FormatException(
'Public key hex must be ${pubKeySize * 2} chars, got ${hex.length}',
);
}
final result = Uint8List(pubKeySize);
for (int i = 0; i < pubKeySize && i * 2 + 1 < hex.length; i++) {
for (int i = 0; i < pubKeySize; i++) {
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
}
return result;
@@ -945,7 +956,7 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
writer.writeBytes(Uint8List(3)); // reserved bytes
writer.writeBytes(pubKey);
} else {
writer.writeBytes(Uint8List(4)); // reserved bytes
writer.writeBytes(Uint8List(3)); // reserved bytes
}
return writer.toBytes();
}
+4
View File
@@ -3,6 +3,10 @@ class MeshCoreUuids {
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
/// Known advertised-name prefixes used by stock MeshCore firmware builds.
/// Discovery no longer filters on these (it filters on the [service] UUID so
/// that community forks with custom names are still found); kept for
/// reference and possible future display heuristics.
static const List<String> deviceNamePrefixes = [
"MeshCore-",
"Whisper-",
+209 -9
View File
@@ -96,6 +96,34 @@ class CayenneLpp {
}
switch (type) {
case lppDigitalInput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppDigitalOutput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppAnalogInput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppAnalogOutput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppGenericSensor:
telemetry.add({
'channel': channel,
@@ -131,6 +159,17 @@ class CayenneLpp {
'value': buffer.readUInt8() / 2,
});
break;
case lppAccelerometer:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'x': buffer.readInt16BE() / 1000,
'y': buffer.readInt16BE() / 1000,
'z': buffer.readInt16BE() / 1000,
},
});
break;
case lppBarometricPressure:
telemetry.add({
'channel': channel,
@@ -138,6 +177,13 @@ class CayenneLpp {
'value': buffer.readUInt16BE() / 10,
});
break;
case lppAltitude:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE(),
});
break;
case lppVoltage:
telemetry.add({
'channel': channel,
@@ -152,6 +198,13 @@ class CayenneLpp {
'value': buffer.readInt16BE() / 1000,
});
break;
case lppFrequency:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppPercentage:
telemetry.add({
'channel': channel,
@@ -173,6 +226,56 @@ class CayenneLpp {
'value': buffer.readUInt16BE(),
});
break;
case lppDistance:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE() / 1000,
});
break;
case lppEnergy:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE() / 1000,
});
break;
case lppDirection:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppUnixTime:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppGyrometer:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'x': buffer.readInt16BE() / 100,
'y': buffer.readInt16BE() / 100,
'z': buffer.readInt16BE() / 100,
},
});
break;
case lppColour:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'red': buffer.readUInt8(),
'green': buffer.readUInt8(),
'blue': buffer.readUInt8(),
},
});
break;
case lppGps:
telemetry.add({
'channel': channel,
@@ -184,6 +287,24 @@ class CayenneLpp {
},
});
break;
case lppSwitch:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppPolyline:
final size = buffer.readUInt8();
telemetry.add({
'channel': channel,
'type': type,
'value': {
'size': size,
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
},
});
break;
default:
return telemetry;
}
@@ -216,6 +337,19 @@ class CayenneLpp {
);
switch (type) {
case lppDigitalInput:
channelData['values']['digitalInput'] = buffer.readUInt8();
break;
case lppDigitalOutput:
channelData['values']['digitalOutput'] = buffer.readUInt8();
break;
case lppAnalogInput:
channelData['values']['analogInput'] = buffer.readInt16BE() / 100.0;
break;
case lppAnalogOutput:
channelData['values']['analogOutput'] =
buffer.readInt16BE() / 100.0;
break;
case lppGenericSensor:
channelData['values']['generic'] = buffer.readUInt32BE();
break;
@@ -231,15 +365,29 @@ class CayenneLpp {
case lppRelativeHumidity:
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
break;
case lppAccelerometer:
channelData['values']['accelerometer'] = {
'x': buffer.readInt16BE() / 1000.0,
'y': buffer.readInt16BE() / 1000.0,
'z': buffer.readInt16BE() / 1000.0,
};
break;
case lppBarometricPressure:
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
break;
case lppAltitude:
// MeshCore encodes standalone barometric altitude as LPP type 121.
channelData['values']['altitude'] = buffer.readInt16BE();
break;
case lppVoltage:
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
break;
case lppCurrent:
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
break;
case lppFrequency:
channelData['values']['frequency'] = buffer.readUInt32BE();
break;
case lppPercentage:
channelData['values']['percentage'] = buffer.readUInt8();
break;
@@ -249,6 +397,32 @@ class CayenneLpp {
case lppPower:
channelData['values']['power'] = buffer.readUInt16BE();
break;
case lppDistance:
channelData['values']['distance'] = buffer.readUInt32BE() / 1000.0;
break;
case lppEnergy:
channelData['values']['energy'] = buffer.readUInt32BE() / 1000.0;
break;
case lppDirection:
channelData['values']['direction'] = buffer.readUInt16BE();
break;
case lppUnixTime:
channelData['values']['time'] = buffer.readUInt32BE();
break;
case lppGyrometer:
channelData['values']['gyrometer'] = {
'x': buffer.readInt16BE() / 100.0,
'y': buffer.readInt16BE() / 100.0,
'z': buffer.readInt16BE() / 100.0,
};
break;
case lppColour:
channelData['values']['colour'] = {
'red': buffer.readUInt8(),
'green': buffer.readUInt8(),
'blue': buffer.readUInt8(),
};
break;
case lppGps:
channelData['values']['gps'] = {
'latitude': buffer.readInt24BE() / 10000.0,
@@ -256,22 +430,48 @@ class CayenneLpp {
'altitude': buffer.readInt24BE() / 100.0,
};
break;
// Add more types as needed...
case lppSwitch:
channelData['values']['switch'] = buffer.readUInt8() != 0;
break;
case lppPolyline:
final size = buffer.readUInt8();
channelData['values']['polyline'] = {
'size': size,
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
};
break;
default:
//Stopped parsing to avoid misalignment
return channels.values.toList();
// Stop parsing to avoid losing alignment on an unknown LPP type.
return _sortedChannelValues(channels);
}
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
return _sortedChannelValues(channels);
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
return <
Map<String, dynamic>
>[]; // Return an empty list on error to avoid crashing the app
// Preserve any fields parsed before the malformed value.
return _sortedChannelValues(channels);
}
}
static Uint8List _readPolylinePayload(BufferReader buffer, int size) {
final declaredPayloadSize = size > 0 ? size - 1 : 0;
final availablePayloadSize = declaredPayloadSize <= buffer.remaining
? declaredPayloadSize
: buffer.remaining;
return buffer.readBytes(availablePayloadSize);
}
static List<Map<String, dynamic>> _sortedChannelValues(
Map<int, Map<String, dynamic>> channels,
) {
final channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
}
static String _bytesToHex(Uint8List bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_protocol.dart';
import '../utils/emoji_utils.dart';
IconData contactTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color contactTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
}
Color colorForName(String name) {
const colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.amber,
Colors.deepOrange,
];
return colors[name.hashCode.abs() % colors.length];
}
String firstCharacterOrEmoji(String name) {
if (name.isEmpty) return '?';
final emoji = firstEmoji(name);
if (emoji != null) return emoji;
final runes = name.runes.toList();
if (runes.isEmpty) return '?';
return String.fromCharCode(runes[0]).toUpperCase();
}
+21 -4
View File
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import '../models/contact.dart';
import '../connector/meshcore_protocol.dart';
import '../models/contact.dart';
class PathHelper {
static String formatPathHex(List<int> pathBytes) {
@@ -9,12 +10,30 @@ class PathHelper {
.join(',');
}
static String hopHex(int byte) {
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
}
static String formatHopHex(List<int> hopBytes) {
return hopBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join();
}
static String? hopName(int byte, List<Contact> allContacts) {
final matches = allContacts
.where(
(c) =>
c.publicKey.isNotEmpty &&
c.publicKey.first == byte &&
(c.type == advTypeRepeater || c.type == advTypeRoom),
)
.toList();
if (matches.isEmpty) return null;
if (matches.length == 1) return matches.first.name;
return matches.map((c) => c.name).join(' | ');
}
static List<Uint8List> splitPathBytes(
List<int> pathBytes,
int hashByteWidth,
@@ -34,7 +53,7 @@ class PathHelper {
}
/// Resolves path bytes to contact names, supporting multi-byte hash widths.
///
/// Groups path bytes according to [hashByteWidth]:
/// - 1: Single byte per hop (256 unique nodes)
/// - 2: Two bytes per hop (65K unique nodes)
@@ -52,11 +71,9 @@ class PathHelper {
for (final hopBytes in splitPathBytes(pathBytes, hashByteWidth)) {
final hex = formatHopHex(hopBytes);
// Find matching contacts by comparing public key prefix
final matches = allContacts.where((c) {
if (c.publicKey.length < hopBytes.length) return false;
if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
// Compare bytes using listEquals for multi-byte support
return listEquals(c.publicKey.sublist(0, hopBytes.length), hopBytes);
}).toList();
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Изтрий",
"common_enable": "Активирай",
"common_disable": "Деактивирай",
"common_autoRefresh": "Автоматично обновяване",
"common_interval": "Интервал",
"common_reboot": "Рестартирай",
"common_loading": "Зареждане...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Цифров вход",
"telemetry_digitalOutputLabel": "Цифров изход",
"telemetry_analogInputLabel": "Аналогов вход",
"telemetry_analogOutputLabel": "Аналогов изход",
"telemetry_genericLabel": "Общ сензор",
"telemetry_luminosityLabel": "Осветеност",
"telemetry_presenceLabel": "Присъствие",
"telemetry_humidityLabel": "Влажност",
"telemetry_accelerometerLabel": "Акселерометър",
"telemetry_pressureLabel": "Налягане",
"telemetry_altitudeLabel": "Надморска височина",
"telemetry_frequencyLabel": "Честота",
"telemetry_percentageLabel": "Процент",
"telemetry_concentrationLabel": "Концентрация",
"telemetry_powerLabel": "Мощност",
"telemetry_distanceLabel": "Разстояние",
"telemetry_energyLabel": "Енергия",
"telemetry_directionLabel": "Посока",
"telemetry_timeLabel": "Време",
"telemetry_gyrometerLabel": "Жироскоп",
"telemetry_colourLabel": "Цвят",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Превключвател",
"telemetry_polylineLabel": "Полилиния",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Брой заявки",
"telemetry_error": "Неуспешно получаване на данни",
"telemetry_noData": "Няма налични данни за телеметрията.",
"telemetry_channelTitle": "Канал {channel}",
"@telemetry_channelTitle": {
@@ -2314,5 +2353,98 @@
"settings_companionDebugLogSubtitle": "Команди, отговори и сурови данни за протоколите BLE/TCP/USB",
"chat_newMessages": "Нови съобщения",
"settings_companionDebugLog": "Лог за отстраняване на грешки (за съпътстваща програма)",
"repeater_chanUtil": "Използване на канала"
"repeater_chanUtil": "Използване на канала",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_pending": "Изпращане",
"common_undo": "Отмяни",
"messageStatus_delivered": "Доставен",
"messageStatus_sent": "Изпратено",
"messageStatus_failed": "Не успях да изпратя",
"messageStatus_repeated": "Слушах го многократно",
"contacts_moreOptions": "Повече възможности",
"contacts_searchOpen": "Търсене на контакти",
"contacts_searchClose": "Затвори търсене",
"routing_title": "Маршрутизиране",
"routing_modeAuto": "Автомобил",
"routing_modeFlood": "Наводнение",
"routing_modeManual": "Ръководство",
"routing_modeAutoHint": "Автоматично избира най-известния път, като при липса на информация, използва стратегия за \"запълване\" на празните пространства.",
"routing_modeFloodHint": "Излъчване през всички ретранслатори. Най-надежният начин, но изисква повече време на въздуха.",
"routing_modeManualHint": "Винаги следва точно пътя, който сте определили.",
"routing_currentRoute": "Текущ маршрут",
"routing_directNoHops": "Директ – без превключватели",
"routing_noPathYet": "Все още няма път. Съобщението продължава да се изпраща, докато не бъде открит маршрут.",
"routing_floodBroadcast": "Предаване през всички ретранслатори",
"routing_editPath": "Редактиране на пътя",
"routing_forgetPath": "Забравете за пътя",
"routing_knownPaths": "Известни маршрути",
"routing_knownPathsHint": "Натиснете бутона, за да превключите към него.",
"routing_inUse": "В експлоатация",
"routing_qualityStrong": "Силен първи скок",
"routing_qualityGood": "Добър първи опит",
"routing_qualityFair": "Първият добър скок",
"routing_qualityWorked": "Беше изпълнено/Доведено до край",
"routing_qualityFlood": "Получено чрез информация, разпространена в резултат на навод.",
"routing_qualityUntested": "Не тестван",
"routing_neverWorked": "никога не е потвърдено",
"routing_floodDelivery": "Доставка при навод",
"pathEditor_title": "Създаване на път",
"pathEditor_hopCounter": "{count} от 64 различни вида малц",
"pathEditor_noHops": "Все още няма добавени хмел. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите рецептата без хмел, за да я изпратите директно.",
"pathEditor_addHops": "Добавете хмела в реда, в който е посочено.",
"pathEditor_searchRepeaters": "Търсене на повтори",
"pathEditor_advancedHex": "Разширено: необработен шестничен път",
"pathEditor_hexLabel": "Префикси на шестнадесетична система",
"pathEditor_hexHelper": "Два шест-символни идентификатора на скок, разделени със запетаи",
"pathEditor_invalidTokens": "Невалидно: {tokens}",
"routing_lastWorked": "worked {when}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "Максимум 64 крачета",
"pathEditor_usePath": "Използвайте този маршрут.",
"pathEditor_removeHop": "Премахнете хмела",
"pathEditor_unknownHop": "Неизвестен репитер",
"map_zoomIn": "Увеличи",
"map_zoomOut": "Приближете се по-малко",
"map_centerMap": "Карта на центъра",
"chrome_bluetoothRequiresChromium": "Web Bluetooth изисква браузър, базиран на Chromium.",
"channels_communityShortId": "Идентификационен номер: {id}...",
"pathTrace_legendGpsConfirmed": "GPS потвърдено",
"pathTrace_legendInferred": "Извлечена позиция"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Löschen",
"common_enable": "Aktivieren",
"common_disable": "Deaktivieren",
"common_autoRefresh": "Automatische Aktualisierung",
"common_interval": "Intervall",
"common_reboot": "Neustart",
"common_loading": "Laden...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Digitaleingang",
"telemetry_digitalOutputLabel": "Digitalausgang",
"telemetry_analogInputLabel": "Analogeingang",
"telemetry_analogOutputLabel": "Analogausgang",
"telemetry_genericLabel": "Allgemeiner Sensor",
"telemetry_luminosityLabel": "Helligkeit",
"telemetry_presenceLabel": "Anwesenheit",
"telemetry_humidityLabel": "Luftfeuchtigkeit",
"telemetry_accelerometerLabel": "Beschleunigungsmesser",
"telemetry_pressureLabel": "Druck",
"telemetry_altitudeLabel": "Höhe",
"telemetry_frequencyLabel": "Frequenz",
"telemetry_percentageLabel": "Prozentsatz",
"telemetry_concentrationLabel": "Konzentration",
"telemetry_powerLabel": "Leistung",
"telemetry_distanceLabel": "Entfernung",
"telemetry_energyLabel": "Energie",
"telemetry_directionLabel": "Richtung",
"telemetry_timeLabel": "Zeit",
"telemetry_gyrometerLabel": "Gyroskop",
"telemetry_colourLabel": "Farbe",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Schalter",
"telemetry_polylineLabel": "Polylinie",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Anzahl der Anfragen",
"telemetry_error": "Daten konnten nicht abgerufen werden",
"telemetry_noData": "Keine Telemetriedaten verfügbar.",
"telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": {
@@ -2342,5 +2381,98 @@
"chat_newMessages": "Neue Nachrichten",
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
"repeater_chanUtil": "Nutzung des Kanals"
"repeater_chanUtil": "Nutzung des Kanals",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Gesendet",
"messageStatus_delivered": "Geliefert",
"common_undo": "Rückgängig machen",
"messageStatus_pending": "Versenden",
"messageStatus_failed": "Nicht gesendet",
"messageStatus_repeated": "Wiederholt gehört",
"contacts_moreOptions": "Weitere Optionen",
"contacts_searchOpen": "Kontakte suchen",
"contacts_searchClose": "Erweiterte Suche",
"routing_title": "Routenplanung",
"routing_modeAuto": "Auto",
"routing_modeFlood": "Überschwemmung",
"routing_modeManual": "Handbuch",
"routing_modeFloodHint": "Übertragung über alle Repeater. Die zuverlässigste Methode, jedoch mit höherem Datenverbrauch.",
"routing_modeAutoHint": "Wählt automatisch den bekanntesten Pfad aus und verwendet eine Flutungsmethode, wenn kein Pfad bekannt ist.",
"routing_modeManualHint": "Sendet immer genau den von Ihnen festgelegten Weg.",
"routing_currentRoute": "Aktuelle Route",
"routing_directNoHops": "Direkt ohne Zwischenverstärkung",
"routing_noPathYet": "Noch kein Pfad gefunden. Die Nachricht wird gesendet, bis ein Weg entdeckt wurde.",
"routing_floodBroadcast": "Übertragung über jeden Repeater",
"routing_editPath": "Pfad bearbeiten",
"routing_forgetPath": "Vergiss den Weg",
"routing_knownPaths": "Bekannte Routen",
"routing_knownPathsHint": "Wählen Sie den Pfad, um zu diesem zu wechseln.",
"routing_inUse": "Im Gebrauch",
"routing_qualityStrong": "Ein starker erster Sprung",
"routing_qualityGood": "Ein guter erster Schritt",
"routing_qualityFair": "Erster erfolgreicher Schritt",
"routing_qualityWorked": "Hat erfolgreich geliefert",
"routing_qualityFlood": "Information erhalten durch Nachrichten über die Überschwemmung",
"routing_qualityUntested": "Nicht getestet",
"routing_lastWorked": "war beschäftigt {when}",
"routing_neverWorked": "nie bestätigt",
"routing_floodDelivery": "Lieferung bei Überschwemmung",
"pathEditor_title": "Pfad erstellen",
"pathEditor_hopCounter": "{count} von 64 Hopfengewächsen",
"pathEditor_noHops": "Noch keine Hopfen hinzugefügt. Klicken Sie auf die Schaltflächen unten, um sie nacheinander hinzuzufügen, oder speichern Sie die Rezepter ohne Hopfen, um sie direkt zu versenden.",
"pathEditor_addHops": "Fügen Sie die Hopfen in der richtigen Reihenfolge hinzu.",
"pathEditor_searchRepeaters": "Suche nach wiederholten Nachrichten",
"pathEditor_advancedHex": "Fortgeschritten: Roh-Hex-Pfad",
"pathEditor_hexLabel": "Hex-Präfixe",
"pathEditor_hexHelper": "Zwei Hexadezimalzeichen pro Sprung, getrennt durch Kommas",
"pathEditor_invalidTokens": "Ungültig: {tokens}",
"pathEditor_tooManyHops": "Maximal 64 Hopfengreifer",
"pathEditor_usePath": "Verwenden Sie diesen Pfad.",
"pathEditor_removeHop": "Hop entfernen",
"pathEditor_unknownHop": "Unbekannter Repeater",
"map_zoomIn": "Zoomen",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Auszoomen",
"map_centerMap": "Zentralkarte",
"chrome_bluetoothRequiresChromium": "Web Bluetooth benötigt einen Chromium-Browser.",
"channels_communityShortId": "ID: {id}…",
"pathTrace_legendGpsConfirmed": "GPS-Bestätigung",
"pathTrace_legendInferred": "Abgeleitete Position"
}
+205 -73
View File
@@ -28,6 +28,12 @@
"common_remove": "Remove",
"common_enable": "Enable",
"common_disable": "Disable",
"common_undo": "Undo",
"messageStatus_sent": "Sent",
"messageStatus_delivered": "Delivered",
"messageStatus_pending": "Sending",
"messageStatus_failed": "Failed to send",
"messageStatus_repeated": "Heard repeated",
"common_reboot": "Reboot",
"common_loading": "Loading...",
"common_notAvailable": "—",
@@ -47,6 +53,8 @@
}
}
},
"common_autoRefresh": "Autorefresh",
"common_interval": "Interval",
"scanner_title": "MeshCore Open",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
@@ -299,17 +307,6 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
"appSettings_maxMessageRetries": "Max Message Retries",
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
"path_routeWeight": "{weight}/{max}",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"appSettings_battery": "Battery",
"appSettings_batteryChemistry": "Battery Chemistry",
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
@@ -455,6 +452,9 @@
}
},
"contacts_newGroup": "New Group",
"contacts_moreOptions": "More options",
"contacts_searchOpen": "Search contacts",
"contacts_searchClose": "Close search",
"contacts_groupName": "Group name",
"contacts_groupNameRequired": "Group name is required",
"contacts_groupNameReserved": "This group name is reserved",
@@ -779,15 +779,6 @@
}
},
"debugFrame_hexDump": "Hex Dump:",
"chat_pathManagement": "Path Management",
"chat_ShowAllPaths": "Show all paths",
"chat_routingMode": "Routing mode",
"chat_autoUseSavedPath": "Auto (use saved path)",
"chat_forceFloodMode": "Force Flood Mode",
"chat_recentAckPaths": "Recent ACK Paths (tap to use):",
"chat_pathHistoryFull": "Path history is full. Remove entries to add new ones.",
"chat_hopSingular": "hop",
"chat_hopPlural": "hops",
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
"@chat_hopsCount": {
"placeholders": {
@@ -796,31 +787,80 @@
}
}
},
"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:",
"chat_setCustomPath": "Set Custom Path",
"chat_setCustomPathSubtitle": "Manually specify routing path",
"chat_clearPath": "Clear Path",
"chat_clearPathSubtitle": "Force rediscovery on next send",
"chat_pathCleared": "Path cleared. Next message will rediscover route.",
"chat_floodModeSubtitle": "Use routing toggle in app bar",
"chat_floodModeEnabled": "Flood mode enabled. Toggle back via routing icon in app bar.",
"chat_fullPath": "Full Path",
"chat_pathDetailsNotAvailable": "Path details not available yet. Try sending a message to refresh.",
"chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
"@chat_pathSetHops": {
"routing_title": "Routing",
"routing_modeAuto": "Auto",
"routing_modeFlood": "Flood",
"routing_modeManual": "Manual",
"routing_modeAutoHint": "Picks the best known path automatically, flooding when none is known.",
"routing_modeFloodHint": "Broadcasts through every repeater. Most reliable, but uses more airtime.",
"routing_modeManualHint": "Always sends along the exact path you set.",
"routing_currentRoute": "Current route",
"routing_directNoHops": "Direct — no repeater hops",
"routing_noPathYet": "No path yet. The next message floods until a route is discovered.",
"routing_floodBroadcast": "Broadcast through every repeater",
"routing_editPath": "Edit path",
"routing_forgetPath": "Forget path",
"routing_knownPaths": "Known paths",
"routing_knownPathsHint": "Tap a path to switch to it.",
"routing_inUse": "In use",
"routing_qualityStrong": "Strong first hop",
"routing_qualityGood": "Good first hop",
"routing_qualityFair": "Fair first hop",
"routing_qualityWorked": "Has delivered",
"routing_qualityFlood": "Heard via flood",
"routing_qualityUntested": "Untested",
"routing_lastWorked": "worked {when}",
"@routing_lastWorked": {
"placeholders": {
"hopCount": {
"type": "int"
},
"status": {
"when": {
"type": "String"
}
}
},
"routing_neverWorked": "never confirmed",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"routing_floodDelivery": "Flood delivery",
"pathEditor_title": "Build Path",
"pathEditor_hopCounter": "{count} of 64 hops",
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"pathEditor_noHops": "No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.",
"pathEditor_addHops": "Add hops in order",
"pathEditor_searchRepeaters": "Search repeaters",
"pathEditor_advancedHex": "Advanced: raw hex path",
"pathEditor_hexLabel": "Hex prefixes",
"pathEditor_hexHelper": "Two hex characters per hop, separated by commas",
"pathEditor_invalidTokens": "Invalid: {tokens}",
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"pathEditor_tooManyHops": "Maximum 64 hops",
"pathEditor_usePath": "Use this path",
"pathEditor_removeHop": "Remove hop",
"pathEditor_unknownHop": "Unknown repeater",
"chat_pathSavedLocally": "Saved locally. Connect to sync.",
"chat_pathDeviceConfirmed": "Device confirmed.",
"chat_pathDeviceNotConfirmed": "Device not confirmed yet.",
@@ -1102,41 +1142,8 @@
"login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.",
"common_reload": "Reload",
"common_clear": "Clear",
"path_currentPath": "Current path: {path}",
"@path_currentPath": {
"placeholders": {
"path": {
"type": "String"
}
}
},
"path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path",
"@path_usingHopsPath": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"path_enterCustomPath": "Enter Custom Path",
"path_currentPathLabel": "Current path",
"path_hexPrefixInstructions": "Enter 2-character hex prefixes for each hop, separated by commas.",
"path_hexPrefixExample": "Example: A1,F2,3C (each node uses first byte of its public key)",
"path_labelHexPrefixes": "Path (hex prefixes)",
"path_helperMaxHops": "Max 64 hops. Each prefix is 2 hex characters (1 byte)",
"path_selectFromContacts": "Or select from contacts:",
"path_noRepeatersFound": "No repeaters or room servers found.",
"path_customPathsRequire": "Custom paths require intermediate hops that can relay messages.",
"path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}",
"@path_invalidHexPrefixes": {
"placeholders": {
"prefixes": {
"type": "String"
}
}
},
"path_tooLong": "Path too long. Maximum 64 hops allowed.",
"path_setPath": "Set Path",
"repeater_management": "Repeater Management",
"room_management": "Room Server Management",
"repeater_guest": "Repeater Information",
@@ -1163,9 +1170,6 @@
},
"repeater_statusTitle": "Repeater Status",
"repeater_routingMode": "Routing mode",
"repeater_autoUseSavedPath": "Auto (use saved path)",
"repeater_forceFloodMode": "Force Flood Mode",
"repeater_pathManagement": "Path management",
"repeater_refresh": "Refresh",
"repeater_statusRequestTimeout": "Status request timed out.",
"repeater_errorLoadingStatus": "Error loading status: {error}",
@@ -1665,6 +1669,120 @@
}
}
},
"telemetry_digitalInputLabel": "Digital Input",
"telemetry_digitalOutputLabel": "Digital Output",
"telemetry_analogInputLabel": "Analog Input",
"telemetry_analogOutputLabel": "Analog Output",
"telemetry_genericLabel": "Generic Sensor",
"telemetry_luminosityLabel": "Luminosity",
"telemetry_presenceLabel": "Presence",
"telemetry_humidityLabel": "Humidity",
"telemetry_accelerometerLabel": "Accelerometer",
"telemetry_pressureLabel": "Pressure",
"telemetry_altitudeLabel": "Altitude",
"telemetry_frequencyLabel": "Frequency",
"telemetry_percentageLabel": "Percentage",
"telemetry_concentrationLabel": "Concentration",
"telemetry_powerLabel": "Power",
"telemetry_distanceLabel": "Distance",
"telemetry_energyLabel": "Energy",
"telemetry_directionLabel": "Direction",
"telemetry_timeLabel": "Time",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Colour",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Switch",
"telemetry_polylineLabel": "Polyline",
"telemetry_altitudeValue": "{meters} m",
"@telemetry_altitudeValue": {
"placeholders": {
"meters": {
"type": "String"
}
}
},
"telemetry_frequencyValue": "{hertz} Hz",
"@telemetry_frequencyValue": {
"placeholders": {
"hertz": {
"type": "String"
}
}
},
"telemetry_pressureValue": "{hpa} hPa",
"@telemetry_pressureValue": {
"placeholders": {
"hpa": {
"type": "String"
}
}
},
"telemetry_luminosityValue": "{lux} lx",
"@telemetry_luminosityValue": {
"placeholders": {
"lux": {
"type": "String"
}
}
},
"telemetry_powerValue": "{watts} W",
"@telemetry_powerValue": {
"placeholders": {
"watts": {
"type": "String"
}
}
},
"telemetry_distanceValue": "{meters} m",
"@telemetry_distanceValue": {
"placeholders": {
"meters": {
"type": "String"
}
}
},
"telemetry_energyValue": "{kilowattHours} kWh",
"@telemetry_energyValue": {
"placeholders": {
"kilowattHours": {
"type": "String"
}
}
},
"telemetry_directionValue": "{degrees}°",
"@telemetry_directionValue": {
"placeholders": {
"degrees": {
"type": "String"
}
}
},
"telemetry_concentrationValue": "{ppm} ppm",
"@telemetry_concentrationValue": {
"placeholders": {
"ppm": {
"type": "String"
}
}
},
"telemetry_percentageValue": "{percent}%",
"@telemetry_percentageValue": {
"placeholders": {
"percent": {
"type": "String"
}
}
},
"telemetry_analogValue": "{value}",
"@telemetry_analogValue": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"telemetry_autoFetchQuantity": "Requests quantity",
"telemetry_error": "Unable to retrieve data",
"neighbors_receivedData": "Received Neighbors Data",
"neighbors_requestTimedOut": "Neighbors request timed out.",
"neighbors_errorLoading": "Error loading neighbors: {error}",
@@ -2373,5 +2491,19 @@
"contact_typeRepeater": "Repeater",
"contact_typeRoom": "Room",
"contact_typeSensor": "Sensor",
"contact_typeUnknown": "Unknown"
"contact_typeUnknown": "Unknown",
"map_zoomIn": "Zoom in",
"map_zoomOut": "Zoom out",
"map_centerMap": "Center map",
"chrome_bluetoothRequiresChromium": "Web Bluetooth requires a Chromium browser",
"channels_communityShortId": "ID: {id}...",
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"pathTrace_legendGpsConfirmed": "GPS confirmed",
"pathTrace_legendInferred": "Inferred position"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Eliminar",
"common_enable": "Activar",
"common_disable": "Desactivar",
"common_autoRefresh": "Actualización automática",
"common_interval": "Intervalo",
"common_reboot": "Reiniciar",
"common_loading": "Cargando...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Entrada digital",
"telemetry_digitalOutputLabel": "Salida digital",
"telemetry_analogInputLabel": "Entrada analógica",
"telemetry_analogOutputLabel": "Salida analógica",
"telemetry_genericLabel": "Sensor genérico",
"telemetry_luminosityLabel": "Luminosidad",
"telemetry_presenceLabel": "Presencia",
"telemetry_humidityLabel": "Humedad",
"telemetry_accelerometerLabel": "Acelerómetro",
"telemetry_pressureLabel": "Presión",
"telemetry_altitudeLabel": "Altitud",
"telemetry_frequencyLabel": "Frecuencia",
"telemetry_percentageLabel": "Porcentaje",
"telemetry_concentrationLabel": "Concentración",
"telemetry_powerLabel": "Potencia",
"telemetry_distanceLabel": "Distancia",
"telemetry_energyLabel": "Energía",
"telemetry_directionLabel": "Dirección",
"telemetry_timeLabel": "Hora",
"telemetry_gyrometerLabel": "Girómetro",
"telemetry_colourLabel": "Color",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Interruptor",
"telemetry_polylineLabel": "Polilínea",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Número de solicitudes",
"telemetry_error": "No se pudieron obtener los datos",
"telemetry_noData": "No hay datos de telemetría disponibles.",
"telemetry_channelTitle": "Canal {channel}",
"@telemetry_channelTitle": {
@@ -2342,5 +2381,98 @@
"chat_newMessages": "Nuevos mensajes",
"settings_companionDebugLogSubtitle": "Comandos, respuestas y datos brutos para protocolos BLE/TCP/USB",
"chat_markAsUnread": "Marcar como no leído",
"repeater_chanUtil": "Utilización del canal"
"repeater_chanUtil": "Utilización del canal",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_pending": "Enviar",
"common_undo": "Deshacer",
"messageStatus_sent": "Sentido",
"messageStatus_delivered": "Entregado",
"messageStatus_failed": "No se pudo enviar",
"messageStatus_repeated": "Escuché repetidamente",
"contacts_moreOptions": "Más opciones",
"contacts_searchOpen": "Buscar contactos",
"contacts_searchClose": "Búsqueda avanzada",
"routing_title": "Ruteo",
"routing_modeAuto": "Coche",
"routing_modeFlood": "Inundación",
"routing_modeManual": "Manual",
"routing_modeAutoHint": "Selecciona automáticamente la ruta más conocida, y si no hay ninguna ruta conocida, utiliza la ruta más directa.",
"routing_modeFloodHint": "Transmisiones a través de todos los repetidores. Es la opción más fiable, pero utiliza más tiempo de transmisión.",
"routing_modeManualHint": "Siempre sigue exactamente la ruta que usted ha definido.",
"routing_currentRoute": "Ruta actual",
"routing_directNoHops": "Directo — sin saltos de repetidor",
"routing_noPathYet": "Aún no hay un camino definido. El mensaje se envía continuamente hasta que se encuentre una ruta.",
"routing_floodBroadcast": "Transmisión a través de todos los repetidores.",
"routing_editPath": "Editar ruta",
"routing_forgetPath": "Olvídate del camino",
"routing_knownPaths": "Rutas conocidas",
"routing_knownPathsHint": "Seleccione una opción para cambiar a esa.",
"routing_inUse": "En uso",
"routing_qualityStrong": "Primer salto exitoso",
"routing_qualityGood": "Primer paso exitoso",
"routing_qualityWorked": "Ha cumplido",
"routing_qualityFair": "Primer salto de calidad",
"routing_qualityFlood": "Se ha escuchado a través de rumores.",
"routing_qualityUntested": "Sin probar",
"routing_lastWorked": "trabajó {when}",
"routing_neverWorked": "nunca confirmado",
"routing_floodDelivery": "Entrega por inundación",
"pathEditor_title": "Crear ruta",
"pathEditor_hopCounter": "{count} de 64 granos de lúpulo",
"pathEditor_noHops": "Aún no se han añadido lúpulos. Haga clic en los repetidores para añadirlos en el orden deseado, o guarde la receta sin lúpulos para enviarla directamente.",
"pathEditor_addHops": "Añadir los lúpulos en el orden adecuado.",
"pathEditor_searchRepeaters": "Buscar repetidores",
"pathEditor_advancedHex": "Avanzado: ruta hexadecimal sin procesar",
"pathEditor_hexLabel": "Prefijos hexadecimales",
"pathEditor_hexHelper": "Dos caracteres hexadecimales por salto, separados por comas.",
"pathEditor_invalidTokens": "Inválido: {tokens}",
"pathEditor_tooManyHops": "Máximo 64 saltos",
"pathEditor_usePath": "Utilice esta ruta.",
"pathEditor_removeHop": "Eliminar el lúpulo",
"pathEditor_unknownHop": "Repetidor desconocido",
"map_zoomIn": "Acercar",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Acercar",
"map_centerMap": "Mapa del centro",
"chrome_bluetoothRequiresChromium": "Web Bluetooth requiere un navegador Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "Confirmado mediante GPS",
"pathTrace_legendInferred": "Posición inferida"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Supprimer",
"common_enable": "Activer",
"common_disable": "Désactiver",
"common_autoRefresh": "Actualisation automatique",
"common_interval": "Intervalle",
"common_reboot": "Redémarrer",
"common_loading": "Chargement...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Entrée numérique",
"telemetry_digitalOutputLabel": "Sortie numérique",
"telemetry_analogInputLabel": "Entrée analogique",
"telemetry_analogOutputLabel": "Sortie analogique",
"telemetry_genericLabel": "Capteur générique",
"telemetry_luminosityLabel": "Luminosité",
"telemetry_presenceLabel": "Présence",
"telemetry_humidityLabel": "Humidité",
"telemetry_accelerometerLabel": "Accéléromètre",
"telemetry_pressureLabel": "Pression",
"telemetry_altitudeLabel": "Altitude",
"telemetry_frequencyLabel": "Fréquence",
"telemetry_percentageLabel": "Pourcentage",
"telemetry_concentrationLabel": "Concentration",
"telemetry_powerLabel": "Puissance",
"telemetry_distanceLabel": "Distance",
"telemetry_energyLabel": "Énergie",
"telemetry_directionLabel": "Direction",
"telemetry_timeLabel": "Heure",
"telemetry_gyrometerLabel": "Gyromètre",
"telemetry_colourLabel": "Couleur",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Interrupteur",
"telemetry_polylineLabel": "Polyligne",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Nombre de requêtes",
"telemetry_error": "Impossible de récupérer les données",
"telemetry_noData": "Aucune donnée de télémétrie disponible.",
"telemetry_channelTitle": "Canal {channel}",
"@telemetry_channelTitle": {
@@ -2321,5 +2360,98 @@
"chat_markAsUnread": "Signaler comme non lu",
"chat_newMessages": "Nouveaux messages",
"settings_companionDebugLogSubtitle": "Commandes, réponses et données brutes pour les protocoles BLE/TCP/USB",
"repeater_chanUtil": "Utilisation du canal"
"repeater_chanUtil": "Utilisation du canal",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"common_undo": "Annuler",
"messageStatus_sent": "Envoyer",
"messageStatus_delivered": "Livré",
"messageStatus_pending": "Envoyer",
"messageStatus_failed": "Échec de l'envoi",
"messageStatus_repeated": "Répété plusieurs fois",
"contacts_searchOpen": "Rechercher des contacts",
"contacts_moreOptions": "Plus d'options",
"contacts_searchClose": "Recherche avancée",
"routing_title": "Planification des itinéraires",
"routing_modeAuto": "Voiture",
"routing_modeFlood": "Inondation",
"routing_modeManual": "Manuel",
"routing_modeFloodHint": "Diffusion via tous les répéteurs. La méthode la plus fiable, mais qui utilise plus de temps d'antenne.",
"routing_modeAutoHint": "Sélectionne automatiquement le chemin le plus connu, et utilise la méthode de \"inondation\" si aucun chemin n'est connu.",
"routing_modeManualHint": "Il suit toujours le chemin précis que vous avez défini.",
"routing_currentRoute": "Itinéraire actuel",
"routing_directNoHops": "Direct — sans relais",
"routing_noPathYet": "Aucune voie encore trouvée. Le message suivant est envoyé jusqu'à ce qu'une route soit découverte.",
"routing_floodBroadcast": "Diffusion via tous les répéteurs",
"routing_editPath": "Modifier le chemin",
"routing_forgetPath": "Oubliez le chemin",
"routing_knownPaths": "Chemins connus",
"routing_knownPathsHint": "Créez un raccourci pour y accéder.",
"routing_inUse": "En cours d'utilisation",
"routing_qualityStrong": "Première étape réussie",
"routing_qualityGood": "Première étape réussie",
"routing_qualityFair": "Première étape réussie",
"routing_qualityWorked": "A livré",
"routing_qualityFlood": "Rapporté par des informations provenant de plusieurs sources.",
"routing_qualityUntested": "Non testé",
"routing_lastWorked": "a travaillé {when}",
"routing_neverWorked": "jamais confirmé",
"routing_floodDelivery": "Livraison en cas de inondation",
"pathEditor_hopCounter": "{count} parmi 64 houblons",
"pathEditor_title": "Créer un chemin",
"pathEditor_noHops": "Aucun houblon ajouté pour le moment. Cliquez sur les répétiteurs ci-dessous pour les ajouter dans l'ordre souhaité, ou enregistrez sans houblon pour envoyer directement.",
"pathEditor_addHops": "Ajoutez les houblons dans l'ordre souhaité.",
"pathEditor_searchRepeaters": "Rechercher des répétiteurs",
"pathEditor_advancedHex": "Avancé : chemin hexadécimal brut",
"pathEditor_hexLabel": "Préfixes hexadécimaux",
"pathEditor_hexHelper": "Deux caractères hexadécimaux par saut, séparés par des virgules.",
"pathEditor_invalidTokens": "Incorrect : {tokens}",
"pathEditor_tooManyHops": "Maximum 64 sauts",
"pathEditor_usePath": "Utilisez ce chemin.",
"pathEditor_removeHop": "Éliminer le haricot",
"pathEditor_unknownHop": "Répéteur non identifié",
"map_zoomIn": "Zoomez",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Zoomez",
"map_centerMap": "Carte du centre",
"chrome_bluetoothRequiresChromium": "Web Bluetooth nécessite un navigateur Chromium.",
"channels_communityShortId": "ID : {id}…",
"pathTrace_legendGpsConfirmed": "Le GPS a confirmé.",
"pathTrace_legendInferred": "Position déduite"
}
+133 -1
View File
@@ -27,6 +27,8 @@
"common_remove": "Eltávolít",
"common_enable": "Engedélyezés",
"common_disable": "Leteteszt",
"common_autoRefresh": "Automatikus frissítés",
"common_interval": "Intervallum",
"common_reboot": "Újraindítás",
"common_loading": "Betöltés...",
"common_notAvailable": "—",
@@ -1466,6 +1468,43 @@
}
}
},
"telemetry_digitalInputLabel": "Digitális bemenet",
"telemetry_digitalOutputLabel": "Digitális kimenet",
"telemetry_analogInputLabel": "Analóg bemenet",
"telemetry_analogOutputLabel": "Analóg kimenet",
"telemetry_genericLabel": "Általános érzékelő",
"telemetry_luminosityLabel": "Fényerő",
"telemetry_presenceLabel": "Jelenlét",
"telemetry_humidityLabel": "Páratartalom",
"telemetry_accelerometerLabel": "Gyorsulásmérő",
"telemetry_pressureLabel": "Nyomás",
"telemetry_altitudeLabel": "Magasság",
"telemetry_frequencyLabel": "Frekvencia",
"telemetry_percentageLabel": "Százalék",
"telemetry_concentrationLabel": "Koncentráció",
"telemetry_powerLabel": "Teljesítmény",
"telemetry_distanceLabel": "Távolság",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Irány",
"telemetry_timeLabel": "Idő",
"telemetry_gyrometerLabel": "Giroszkóp",
"telemetry_colourLabel": "Szín",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Kapcsoló",
"telemetry_polylineLabel": "Töröttvonal",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Kérések száma",
"telemetry_error": "Nem sikerült lekérni az adatokat",
"telemetry_noData": "Nincsenek elérhető telemetriadatok.",
"telemetry_channelTitle": "{channel} csatorna",
"@telemetry_channelTitle": {
@@ -2352,5 +2391,98 @@
"chat_newMessages": "Új üzenetek",
"settings_companionDebugLog": "Párhuzamos hibakeresési napló",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB parancsok, válaszok és alapvető adatok",
"repeater_chanUtil": "Csatorna-használat"
"repeater_chanUtil": "Csatorna-használat",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_delivered": "Szállítva",
"common_undo": "Még egyszer",
"messageStatus_sent": "Elküldve",
"messageStatus_pending": "Elküldés",
"messageStatus_failed": "Nem sikerült elküldeni",
"messageStatus_repeated": "Ismételtem",
"contacts_moreOptions": "További lehetőségek",
"contacts_searchOpen": "Keresssz kapcsolatokat",
"contacts_searchClose": "Teljesítse a keresést",
"routing_title": "Útvonal meghatározás",
"routing_modeAuto": "Autó",
"routing_modeFlood": "Áradás",
"routing_modeManual": "Használati útmutató",
"routing_modeAutoHint": "Automatikusan kiválasztja a legismertebb útvonalat, és ha egyik sem ismert, akkor \"vízzel\" tölti ki.",
"routing_modeFloodHint": "Átvisszaadások minden erősítőn keresztül. A legmegbízhatóbb megoldás, de több időt igényel.",
"routing_modeManualHint": "Mindig pontosan az útvonalat követi, amelyet megad.",
"routing_currentRoute": "Jelenlegi útvonal",
"routing_directNoHops": "Közvetlen nincs átjáró állomás",
"routing_noPathYet": "Még nincs útvonal. A következő üzenet a keresésig vár.",
"routing_floodBroadcast": "Azonnali továbbítás minden erősítőn keresztül.",
"routing_editPath": "Útvonal szerkesztése",
"routing_forgetPath": "Felejtsd el a útvonalat",
"routing_knownPaths": "Jellegzetes útvonalak",
"routing_knownPathsHint": "Készíts egy útvonalat, hogy átválhass rá.",
"routing_inUse": "Használatban",
"routing_qualityStrong": "Erős első lépés",
"routing_qualityGood": "Jó első lépés",
"routing_qualityFair": "Jó első lépés",
"routing_qualityWorked": "Előállított",
"routing_qualityFlood": "Információt hallottam a katasztrófa miatt.",
"routing_qualityUntested": "Vizsgálatnak nem подвержен",
"routing_neverWorked": "sosem megerősítve",
"routing_floodDelivery": "Vízparti szállítás",
"pathEditor_title": "Út megépítése",
"pathEditor_hopCounter": "{count} db 64-ből",
"pathEditor_noHops": "Még nem adtam hozzá a bazsalikomot. A lent található gombokat használhatod, hogy sorrendben adjd hozzá, vagy mentheted anélkül, hogy bazsalikomot adnál hozzá, hogy közvetlenül elküldd.",
"pathEditor_addHops": "Adja hozzá a bazsaidat a megfelelő sorrendben.",
"pathEditor_searchRepeaters": "Ismétlő eszközök keresése",
"pathEditor_advancedHex": "Haladó szint: alapvető hex-út",
"pathEditor_hexLabel": "Hex előtagok",
"pathEditor_hexHelper": "Két hatjegyű szám minden lépésen, amelyek egymástól elválasztják a kommák.",
"pathEditor_invalidTokens": "Érvénytelen: {tokens}",
"routing_lastWorked": "worked {when}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "A maximális szám 64.",
"pathEditor_usePath": "Használja ezt az útvonalat.",
"pathEditor_removeHop": "Távolítsa el a bazsalikomot",
"pathEditor_unknownHop": "Tudatlan erősítő",
"map_zoomIn": "Nagyítva",
"map_zoomOut": "Kicsökkentett nézet",
"map_centerMap": "Központi tér térkép",
"chrome_bluetoothRequiresChromium": "A Web Bluetooth-hoz egy Chromium-alapú böngésző szükséges.",
"channels_communityShortId": "Az azonosító: {id}...",
"pathTrace_legendGpsConfirmed": "GPS-en megerősítve",
"pathTrace_legendInferred": "Feltehető helyzet"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Elimina",
"common_enable": "Abilita",
"common_disable": "Disattivare",
"common_autoRefresh": "Aggiornamento automatico",
"common_interval": "Intervallo",
"common_reboot": "Riavvia",
"common_loading": "Caricamento...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Ingresso digitale",
"telemetry_digitalOutputLabel": "Uscita digitale",
"telemetry_analogInputLabel": "Ingresso analogico",
"telemetry_analogOutputLabel": "Uscita analogica",
"telemetry_genericLabel": "Sensore generico",
"telemetry_luminosityLabel": "Luminosità",
"telemetry_presenceLabel": "Presenza",
"telemetry_humidityLabel": "Umidità",
"telemetry_accelerometerLabel": "Accelerometro",
"telemetry_pressureLabel": "Pressione",
"telemetry_altitudeLabel": "Altitudine",
"telemetry_frequencyLabel": "Frequenza",
"telemetry_percentageLabel": "Percentuale",
"telemetry_concentrationLabel": "Concentrazione",
"telemetry_powerLabel": "Potenza",
"telemetry_distanceLabel": "Distanza",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Direzione",
"telemetry_timeLabel": "Ora",
"telemetry_gyrometerLabel": "Giroscopio",
"telemetry_colourLabel": "Colore",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Interruttore",
"telemetry_polylineLabel": "Polilinea",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Numero di richieste",
"telemetry_error": "Impossibile recuperare i dati",
"telemetry_noData": "Nessun dato di telemetria disponibile.",
"telemetry_channelTitle": "Canale {channel}",
"@telemetry_channelTitle": {
@@ -2314,5 +2353,98 @@
"settings_companionDebugLog": "Registro di debug per il supporto",
"chat_newMessages": "Nuovi messaggi",
"chat_markAsUnread": "Segna come non letto",
"repeater_chanUtil": "Utilizzo del canale"
"repeater_chanUtil": "Utilizzo del canale",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"common_undo": "Annulla",
"messageStatus_delivered": "Consegnato",
"messageStatus_sent": "Invia",
"messageStatus_pending": "Invio",
"messageStatus_failed": "Impossibile inviare",
"messageStatus_repeated": "Sentito ripetutamente",
"contacts_moreOptions": "Ulteriori opzioni",
"contacts_searchOpen": "Cerca contatti",
"contacts_searchClose": "Ricerca avanzata",
"routing_title": "Instradamento",
"routing_modeAuto": "Auto",
"routing_modeFlood": "Inondazione",
"routing_modeManual": "Manuale",
"routing_modeAutoHint": "Seleziona automaticamente il percorso più noto, e in caso di assenza di informazioni, utilizza un percorso casuale.",
"routing_modeFloodHint": "Trasmissioni tramite ogni ripetitore. Il metodo più affidabile, ma richiede più tempo di trasmissione.",
"routing_modeManualHint": "Invia sempre esattamente il percorso che hai definito.",
"routing_currentRoute": "Percorso attuale",
"routing_directNoHops": "Diretto — senza passaggi tramite ripetitori",
"routing_noPathYet": "Al momento non è stata individuata alcuna via. Il messaggio viene inviato ripetutamente finché non viene trovata una rotta.",
"routing_floodBroadcast": "Trasmissione attraverso ogni ripetitore",
"routing_editPath": "Percorso di modifica",
"routing_forgetPath": "Dimentica il percorso",
"routing_knownPaths": "Percorsi noti",
"routing_knownPathsHint": "Seleziona un percorso per accedere a questa opzione.",
"routing_inUse": "In uso",
"routing_qualityStrong": "Primo salto molto deciso",
"routing_qualityGood": "Primo tentativo di successo",
"routing_qualityFair": "Primo salto di qualità",
"routing_qualityWorked": "È stato consegnato",
"routing_qualityFlood": "Ho sentito tramite un messaggio urgente",
"routing_qualityUntested": "Non testato",
"routing_neverWorked": "mai confermato",
"routing_floodDelivery": "Consegna in caso di alluvione",
"pathEditor_title": "Creare percorso",
"pathEditor_hopCounter": "{count} tra 64 varietà di luppolo",
"pathEditor_noHops": "Al momento non ci sono ingredienti aggiuntivi. Per aggiungerli nell'ordine desiderato, cliccate sui ripetitori sottostanti. In alternativa, potete salvare la ricetta senza ingredienti aggiuntivi per inviarla direttamente.",
"pathEditor_addHops": "Aggiungere i luppoli nell'ordine desiderato.",
"pathEditor_searchRepeaters": "Ricerca ripetitori",
"pathEditor_advancedHex": "Avanzato: percorso esadecimale grezzo",
"pathEditor_hexLabel": "Prefissi esadecimali",
"pathEditor_hexHelper": "Due caratteri esadecimali per ogni salto, separati da virgole.",
"pathEditor_invalidTokens": "Non valido: {tokens}",
"routing_lastWorked": "worked {when}",
"pathEditor_tooManyHops": "Massimo 64 orari",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_usePath": "Utilizza questo percorso",
"pathEditor_removeHop": "Rimuovere il luppolo",
"pathEditor_unknownHop": "Ripetitore sconosciuto",
"map_zoomIn": "Ingrandisci",
"map_zoomOut": "Riduci la visualizzazione",
"map_centerMap": "Mappa del centro",
"channels_communityShortId": "ID: {id}...",
"chrome_bluetoothRequiresChromium": "Web Bluetooth richiede un browser basato su Chromium.",
"pathTrace_legendGpsConfirmed": "Il GPS conferma",
"pathTrace_legendInferred": "Posizione dedotta"
}
+207 -75
View File
@@ -27,6 +27,8 @@
"common_remove": "削除",
"common_enable": "有効化する",
"common_disable": "無効化する",
"common_autoRefresh": "自動更新",
"common_interval": "間隔",
"common_reboot": "再起動",
"common_loading": "読み込み中...",
"common_notAvailable": "—",
@@ -214,7 +216,7 @@
"settings_txPower": "TX 信号電力 (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "無効な送信電力 (0-22 dBm)",
"settings_clientRepeat": "オフグリッド(電力網から孤立した状態)の繰り返し",
"settings_clientRepeat": "オフグリッドリピータ",
"settings_clientRepeatSubtitle": "このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。",
"settings_clientRepeatFreqWarning": "オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。",
"settings_error": "エラー:{message}",
@@ -268,7 +270,7 @@
"appSettings_pathsWillBeCleared": "5回失敗した後、経路が再開されます。",
"appSettings_pathsWillNotBeCleared": "パスは自動で削除されません。",
"appSettings_autoRouteRotation": "自動ルートの切り替え",
"appSettings_autoRouteRotationSubtitle": "最適なルートと、洪水モードを切り替える",
"appSettings_autoRouteRotationSubtitle": "最適なルートと、フラッドモードを切り替える",
"appSettings_autoRouteRotationEnabled": "自動ルートの切り替え機能が有効になっています",
"appSettings_autoRouteRotationDisabled": "自動ルートの変更機能が無効になっています。",
"appSettings_maxRouteWeight": "最大ルート重量",
@@ -308,7 +310,7 @@
"appSettings_batteryLipo": "LiPo (3.0-4.2V)",
"appSettings_mapDisplay": "地図の表示",
"appSettings_showRepeaters": "繰り返し再生機能",
"appSettings_showRepeatersSubtitle": "地図上にリピータノードを表示する",
"appSettings_showRepeatersSubtitle": "地図上にリピータノードを表示する",
"appSettings_showChatNodes": "チャットノードの表示",
"appSettings_showChatNodesSubtitle": "地図上にチャットノードを表示する",
"appSettings_showOtherNodes": "他のノードを表示する",
@@ -422,7 +424,7 @@
}
}
},
"contacts_manageRepeater": "リピータの管理",
"contacts_manageRepeater": "リピータの管理",
"contacts_manageRoom": "ルームサーバーの管理",
"contacts_roomLogin": "ルームサーバーへのログイン",
"contacts_openChat": "自由な会話",
@@ -733,7 +735,7 @@
"chat_ShowAllPaths": "すべての経路を表示",
"chat_routingMode": "ルーティングモード",
"chat_autoUseSavedPath": "自動 (保存されたパスを使用)",
"chat_forceFloodMode": "強制的に洪水モードを起動",
"chat_forceFloodMode": "強制的にフラッドモードを起動",
"chat_recentAckPaths": "最近使用したACKパス(タップして使用):",
"chat_pathHistoryFull": "パスの履歴は完全です。エントリを削除して、新しいものを追加できます。",
"chat_hopSingular": "ジャンプ",
@@ -756,7 +758,7 @@
"chat_clearPathSubtitle": "次回送信時に、以前の情報を再取得する",
"chat_pathCleared": "経路が確保されました。次のメッセージでルートを再確認します。",
"chat_floodModeSubtitle": "アプリのバーにあるルーティング切り替え機能を使用する",
"chat_floodModeEnabled": "洪水モードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。",
"chat_floodModeEnabled": "フラッドモードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。",
"chat_fullPath": "フルパス",
"chat_pathDetailsNotAvailable": "経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。",
"chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -777,7 +779,7 @@
"chat_path": "道",
"chat_publicKey": "公開鍵",
"chat_compressOutgoingMessages": "送信されるメッセージを圧縮する",
"chat_floodForced": "洪水(強制的な)",
"chat_floodForced": "フラッド(強制的な)",
"chat_directForced": "直接的な(強制的な)",
"chat_hopsForced": "{count} 本のホップ(強制的に採取)",
"@chat_hopsForced": {
@@ -787,7 +789,7 @@
}
}
},
"chat_floodAuto": "洪水 (自動)",
"chat_floodAuto": "フラッド (自動)",
"chat_direct": "直接",
"chat_poiShared": "共有されたPOI",
"chat_unread": "未読: {count}",
@@ -832,7 +834,7 @@
}
},
"map_chat": "チャット",
"map_repeater": "繰り返し送信装置",
"map_repeater": "リピータ",
"map_room": "部屋",
"map_sensor": "センサー",
"map_pinDm": "ピン(DM",
@@ -864,7 +866,7 @@
"map_filterNodes": "フィルタノード",
"map_nodeTypes": "ノードの種類",
"map_chatNodes": "チャットノード",
"map_repeaters": "繰り返し送信装置",
"map_repeaters": "リピータ",
"map_otherNodes": "その他のノード",
"map_keyPrefix": "主要なプレフィックス",
"map_filterByKeyPrefix": "主要なプレフィックスでフィルタリングする",
@@ -877,7 +879,7 @@
"map_lastSeenTime": "最後に確認された時間",
"map_sharedPin": "共有パスワード",
"map_joinRoom": "部屋に参加する",
"map_manageRepeater": "リピータの管理",
"map_manageRepeater": "リピータの管理",
"map_tapToAdd": "ノードをクリックして、パスに追加します。",
"map_runTrace": "パスの追跡を実行",
"map_removeLast": "最後のものを削除",
@@ -1010,12 +1012,12 @@
"login_enterPassword": "パスワードを入力してください",
"login_savePassword": "パスワードを保存する",
"login_savePasswordSubtitle": "パスワードは、このデバイスに安全に保存されます。",
"login_repeaterDescription": "設定やステータスにアクセスするために、リピータのパスワードを入力してください。",
"login_repeaterDescription": "設定やステータスにアクセスするために、リピータのパスワードを入力してください。",
"login_roomDescription": "設定やステータスへのアクセスには、部屋のパスワードを入力してください。",
"login_routing": "経路設定",
"login_routingMode": "ルーティングモード",
"login_autoUseSavedPath": "自動 (保存されたパスを使用)",
"login_forceFloodMode": "強制的に洪水モードを起動",
"login_forceFloodMode": "強制的にフラッドモードを起動",
"login_managePaths": "パスの管理",
"login_login": "ログイン",
"login_attempt": "試行回数:{current}/{max}",
@@ -1064,7 +1066,7 @@
"path_helperMaxHops": "最大64個のホップ。各プレフィックスは2つの16進数文字(1バイト)で構成されています。",
"path_selectFromContacts": "または、連絡先リストから選択してください:",
"path_noRepeatersFound": "繰り返し機能やルームサーバーは見つかりませんでした。",
"path_customPathsRequire": "カスタムパスには、メッセージを中継できる中間地点が必要です。",
"path_customPathsRequire": "カスタムパスには、メッセージを中継できるリピータが必要です。",
"path_invalidHexPrefixes": "無効な16進数プレフィックス: {prefixes}",
"@path_invalidHexPrefixes": {
"placeholders": {
@@ -1075,23 +1077,23 @@
},
"path_tooLong": "経路が長すぎる。最大64回のジャンプのみ許可。",
"path_setPath": "パスを設定",
"repeater_management": "リピータ管理",
"repeater_management": "リピータ管理",
"room_management": "ルームサーバーの管理",
"repeater_managementTools": "管理ツール",
"repeater_status": "ステータス",
"repeater_statusSubtitle": "リピータの状態、統計情報、および隣接するネットワークの情報を表示する",
"repeater_statusSubtitle": "リピータの状態、統計情報、および隣接するネットワークの情報を表示する",
"repeater_telemetry": "テレメトリー",
"repeater_telemetrySubtitle": "センサーおよびシステムの状態に関するテレメトリの表示",
"repeater_cli": "CLI(コマンドラインインターフェース)",
"repeater_cliSubtitle": "リピータへのコマンドを送信する",
"repeater_cliSubtitle": "リピータへのコマンドを送信する",
"repeater_neighbors": "近隣住民",
"repeater_neighborsSubtitle": "ゼロホップの隣接ノードを表示する。",
"repeater_settings": "設定",
"repeater_settingsSubtitle": "リピータのパラメータを設定する",
"repeater_settingsSubtitle": "リピータのパラメータを設定する",
"repeater_statusTitle": "再送ステータス",
"repeater_routingMode": "ルーティングモード",
"repeater_autoUseSavedPath": "自動 (保存されたパスを使用)",
"repeater_forceFloodMode": "強制的に洪水モードを起動",
"repeater_forceFloodMode": "強制的にフラッドモードを起動",
"repeater_pathManagement": "経路管理",
"repeater_refresh": "リフレッシュ",
"repeater_statusRequestTimeout": "ステータス情報の取得に失敗しました。",
@@ -1136,7 +1138,7 @@
}
}
},
"repeater_packetTxTotal": "合計: {total}, 洪水: {flood}, 直接: {direct}",
"repeater_packetTxTotal": "合計: {total}, フラッド: {flood}, 直接: {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -1150,7 +1152,7 @@
}
}
},
"repeater_packetRxTotal": "合計: {total}, 洪水: {flood}, 直接: {direct}",
"repeater_packetRxTotal": "合計: {total}, フラッド: {flood}, 直接: {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -1183,10 +1185,10 @@
}
}
},
"repeater_settingsTitle": "リピータ設定",
"repeater_settingsTitle": "リピータ設定",
"repeater_basicSettings": "基本設定",
"repeater_repeaterName": "送信装置名",
"repeater_repeaterNameHelper": "このリピータの名前",
"repeater_repeaterName": "リピータ名",
"repeater_repeaterNameHelper": "このリピータの名前",
"repeater_adminPassword": "管理者パスワード",
"repeater_adminPasswordHelper": "完全アクセス権のパスワード",
"repeater_guestPassword": "ゲスト用のパスワード",
@@ -1206,7 +1208,7 @@
"repeater_longitudeHelper": "度分表記(例:-122.4194",
"repeater_features": "特徴",
"repeater_packetForwarding": "パケット転送",
"repeater_packetForwardingSubtitle": "リピータがパケットを転送できるように設定する",
"repeater_packetForwardingSubtitle": "リピータがパケットを転送できるように設定する",
"repeater_guestAccess": "ゲストへのアクセス",
"repeater_guestAccessSubtitle": "ゲストへの読み取り専用アクセスを許可する",
"repeater_privacyMode": "プライバシーモード",
@@ -1221,7 +1223,7 @@
}
}
},
"repeater_floodAdvertInterval": "洪水に関する広告の表示間隔",
"repeater_floodAdvertInterval": "フラッドに関する広告の表示間隔",
"repeater_floodAdvertIntervalHours": "{hours} 時間",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1232,15 +1234,15 @@
},
"repeater_encryptedAdvertInterval": "暗号化された広告表示間",
"repeater_dangerZone": "危険区域",
"repeater_rebootRepeater": "リピータを再起動する",
"repeater_rebootRepeaterSubtitle": "リピータデバイスを再起動する",
"repeater_rebootRepeaterConfirm": "本当にこのリピータを再起動したいですか?",
"repeater_rebootRepeater": "リピータを再起動する",
"repeater_rebootRepeaterSubtitle": "リピータデバイスを再起動する",
"repeater_rebootRepeaterConfirm": "本当にこのリピータを再起動したいですか?",
"repeater_regenerateIdentityKey": "IDキーの再生成",
"repeater_regenerateIdentityKeySubtitle": "新しい公開鍵/秘密鍵のペアを生成する",
"repeater_regenerateIdentityKeyConfirm": "これにより、リピータには新しい識別情報が割り当てられます。続行しますか?",
"repeater_regenerateIdentityKeyConfirm": "これにより、リピータには新しい識別情報が割り当てられます。続行しますか?",
"repeater_eraseFileSystem": "ファイルシステムを削除する",
"repeater_eraseFileSystemSubtitle": "リピータファイルシステムをフォーマットする",
"repeater_eraseFileSystemConfirm": "警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!",
"repeater_eraseFileSystemSubtitle": "リピータファイルシステムをフォーマットする",
"repeater_eraseFileSystemConfirm": "警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!",
"repeater_eraseSerialOnly": "Erase機能は、シリアルコンソール経由でのみ利用可能です。",
"repeater_commandSent": "送信されたコマンド: {command}",
"@repeater_commandSent": {
@@ -1268,7 +1270,7 @@
"repeater_refreshMultiAcks": "複数のACKをリフレッシュする",
"repeater_networkHealth": "ネットワークの状態",
"repeater_loopDetect": "ループ検出",
"repeater_loopDetectHelper": "ルーティングループに見えるような、洪水パケットを送信する",
"repeater_loopDetectHelper": "ルーティングループを検知する",
"repeater_loopDetectOff": "オフ",
"repeater_loopDetectMinimal": "最小限の",
"repeater_loopDetectModerate": "適度な",
@@ -1284,16 +1286,16 @@
}
},
"repeater_ownerInfo": "事業者の情報",
"repeater_ownerInfoHelper": "このリピータに関する公開メタデータ",
"repeater_ownerInfoHelper": "このリピータに関する公開メタデータ",
"repeater_refreshOwnerInfo": "オペレーター情報の更新",
"repeater_floodMax": "最大ホップ数",
"repeater_floodMaxHelper": "洪水パケットが移動できる最大ホップ数 (0-64)",
"repeater_floodMaxHelper": "フラッドパケットが移動できる最大ホップ数 (0-64)",
"repeater_advancedSettings": "高度な",
"repeater_advancedSettingsSubtitle": "経験豊富なオペレーター向けの調整ノブ",
"repeater_pathHashMode": "パスハッシュモード",
"repeater_pathHashModeHelper": "このリピータのIDをフラッド経路/ループ検出タグにエンコードするために使用るバイト数。0=1バイト (256 ID、最大64ホップ)、1=2バイト (65,000 ID、最大32ホップ)、2=3バイト (1,600万 ID、最大21ホップ)。v1.14より前のファームウェアは常に1バイト経路を使用していました。v1.14以降は2バイトまたは3バイト経路に設定できます。",
"repeater_pathHashModeHelper": "このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。",
"repeater_txDelay": "フロイド・TXでの遅延",
"repeater_txDelayHelper": "洪水時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。",
"repeater_txDelayHelper": "フラッド時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。",
"repeater_directTxDelay": "直接的なTX遅延",
"repeater_directTxDelayHelper": "直接(フラッドではない)トラフィックに対する再送信間隔を、パケットの空中時間(0~2、デフォルト0.3)の倍数として設定する。",
"repeater_intThresh": "干渉閾値",
@@ -1301,10 +1303,10 @@
"repeater_agcResetInterval": "AGCのリセット間隔",
"repeater_agcResetIntervalHelper": "ラジオの自動ゲイン制御をリセットする頻度について:ゲインが固定状態になった場合に、回復するために、何度リセットするかを設定します。4の倍数でリセットする場合、0を設定すると、定期的なリセットは停止します。",
"repeater_actionsTitle": "行動",
"repeater_sendAdvert": "洪水に関する広告を送信",
"repeater_sendAdvertSubtitle": "ネットワークを通じて、洪水に関する広告を放送する",
"repeater_sendAdvert": "フラッドに関する広告を送信",
"repeater_sendAdvertSubtitle": "ネットワークを通じて、フラッドに関する広告を放送する",
"repeater_sendAdvertZeroHop": "ゼロホップ形式の広告を送信する",
"repeater_sendAdvertZeroHopSubtitle": "ワンホップでの広告放送(中継なし)",
"repeater_sendAdvertZeroHopSubtitle": "ワンホップでの広告放送(リピータなし)",
"repeater_clockSync": "現在、時刻を同期する",
"repeater_clockSyncSubtitle": "スマートフォンの時刻をルーターに設定する",
"repeater_actionSucceeded": "{action} が成功しました",
@@ -1326,7 +1328,7 @@
}
}
},
"repeater_settingsSavedRebootNeeded": "設定を保存しました — リピータを再起動して適用してください",
"repeater_settingsSavedRebootNeeded": "設定を保存しました — リピータを再起動して適用してください",
"repeater_settingsPartialFailure": "設定の一部でエラーが発生しました:{failures}",
"@repeater_settingsPartialFailure": {
"placeholders": {
@@ -1365,7 +1367,7 @@
}
}
},
"repeater_cliTitle": "リピータのコマンドラインインターフェース",
"repeater_cliTitle": "リピータのコマンドラインインターフェース",
"repeater_debugNextCommand": "次のコマンドのデバッグ",
"repeater_commandHelp": "コマンドヘルプ",
"repeater_clearHistory": "明確な歴史",
@@ -1399,14 +1401,14 @@
"repeater_cliHelpClearStats": "さまざまな統計カウンターをゼロにリセットする。",
"repeater_cliHelpSetAf": "空き時間係数を設定します。",
"repeater_cliHelpSetTx": "LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)",
"repeater_cliHelpSetRepeat": "このノードに対するリピータの役割を有効化または無効化します。",
"repeater_cliHelpSetRepeat": "このノードに対するリピータの役割を有効化または無効化します。",
"repeater_cliHelpSetAllowReadOnly": "(ルームサーバー設定)「オン」に設定した場合、空白のパスワードでのログインは可能ですが、ルームへの投稿はできません。(閲覧のみ)",
"repeater_cliHelpSetFloodMax": "インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。",
"repeater_cliHelpSetFloodMax": "インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。",
"repeater_cliHelpSetIntThresh": "干渉閾値を設定します(dB単位)。デフォルト値は14です。0に設定すると、チャンネル間の干渉を検出する機能を無効にします。",
"repeater_cliHelpSetAgcResetInterval": "オートゲインコントローラーのリセット間隔を設定します。 0 に設定すると無効化されます。",
"repeater_cliHelpSetMultiAcks": "「ダブルACK」機能の有効化または無効化を可能にします。",
"repeater_cliHelpSetAdvertInterval": "ローカル(ホップなし)の広告パケットを送信する間隔を分単位で設定します。 0 に設定すると、機能を無効にします。",
"repeater_cliHelpSetFloodAdvertInterval": "洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。",
"repeater_cliHelpSetFloodAdvertInterval": "フラッド広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。",
"repeater_cliHelpSetGuestPassword": "ゲストのパスワードを設定/更新します。(繰り返し利用の場合、ゲストのログインは「統計情報を取得」のリクエストを送信できます)",
"repeater_cliHelpSetName": "広告の名前を設定します。",
"repeater_cliHelpSetLat": "広告表示の地図の緯度を設定します。(度分秒表記)",
@@ -1427,14 +1429,14 @@
"repeater_cliHelpLogStart": "パケットのログ記録を開始し、ファイルシステムに保存する。",
"repeater_cliHelpLogStop": "ファイルシステムへのパケットログの記録を停止する。",
"repeater_cliHelpLogErase": "ファイルシステムからパケットログを削除する。",
"repeater_cliHelpNeighbors": "ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。",
"repeater_cliHelpNeighbors": "ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。",
"repeater_cliHelpNeighborRemove": "隣接リストから、最初に一致するエントリ(pubkeyプレフィックス(16進数)で特定)を削除します。",
"repeater_cliHelpRegion": "(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。",
"repeater_cliHelpRegion": "(特定のシリーズのみ)定義されたすべての地域と、現在のフラッド許可状況を一覧表示します。",
"repeater_cliHelpRegionLoad": "注:これは特殊な複数コマンドの呼び出しです。その後の各コマンドは、地域名であり(スペースを使用して親階層を示し、少なくとも1つのスペースが必要です)、空行/コマンドで終了します。",
"repeater_cliHelpRegionGet": "指定された名前のプレフィックスを持つ地域を検索します(または、グローバルな範囲の場合は「*」)。結果として、「region-name (parent-name) 'F'」と返答します。",
"repeater_cliHelpRegionPut": "指定された名前で、領域の定義を追加または更新します。",
"repeater_cliHelpRegionRemove": "指定された名前を持つ領域の定義を削除します。(正確に一致している必要があり、子領域は存在してはなりません)",
"repeater_cliHelpRegionAllowf": "指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)",
"repeater_cliHelpRegionAllowf": "指定された領域に対して、「フラッド」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)",
"repeater_cliHelpRegionDenyf": "指定された領域における「FLOOD」権限を削除します。(注:現時点では、グローバル/従来の範囲での使用は推奨されません!)",
"repeater_cliHelpRegionHome": "現在の「ホーム」地域に返信します。(まだ適用されていない、将来利用を予定)",
"repeater_cliHelpRegionHomeSet": "「ホーム」地域を設定します。",
@@ -1451,7 +1453,7 @@
"repeater_settingsCategory": "設定",
"repeater_bridge": "橋",
"repeater_logging": "ログ記録",
"repeater_neighborsRepeaterOnly": "近隣住民(リピータのみ)",
"repeater_neighborsRepeaterOnly": "近隣住民(リピータのみ)",
"repeater_regionManagementRepeaterOnly": "地域管理(ブロードキャスト用のみ)",
"repeater_regionNote": "地域レベルでの管理のため、地域定義と権限の管理を行うための機能が導入されました。",
"repeater_gpsManagement": "GPS管理",
@@ -1466,6 +1468,43 @@
}
}
},
"telemetry_digitalInputLabel": "デジタル入力",
"telemetry_digitalOutputLabel": "デジタル出力",
"telemetry_analogInputLabel": "アナログ入力",
"telemetry_analogOutputLabel": "アナログ出力",
"telemetry_genericLabel": "汎用センサー",
"telemetry_luminosityLabel": "照度",
"telemetry_presenceLabel": "在室",
"telemetry_humidityLabel": "湿度",
"telemetry_accelerometerLabel": "加速度計",
"telemetry_pressureLabel": "気圧",
"telemetry_altitudeLabel": "高度",
"telemetry_frequencyLabel": "周波数",
"telemetry_percentageLabel": "パーセント",
"telemetry_concentrationLabel": "濃度",
"telemetry_powerLabel": "電力",
"telemetry_distanceLabel": "距離",
"telemetry_energyLabel": "エネルギー",
"telemetry_directionLabel": "方向",
"telemetry_timeLabel": "時刻",
"telemetry_gyrometerLabel": "ジャイロメーター",
"telemetry_colourLabel": "色",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "スイッチ",
"telemetry_polylineLabel": "ポリライン",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "リクエスト数",
"telemetry_error": "データを取得できません",
"telemetry_noData": "テレメトリデータは利用できません。",
"telemetry_channelTitle": "チャンネル {channel}",
"@telemetry_channelTitle": {
@@ -1528,7 +1567,7 @@
}
}
},
"neighbors_repeatersNeighbors": "繰り返し送信する、近隣",
"neighbors_repeatersNeighbors": "近隣のリピータ",
"neighbors_noData": "近隣のデータは利用できません。",
"neighbors_unknownContact": "不明な {pubkey}",
"@neighbors_unknownContact": {
@@ -1549,12 +1588,12 @@
"channelPath_title": "パケットパス",
"channelPath_viewMap": "地図を表示する",
"channelPath_otherObservedPaths": "観察されたその他の経路",
"channelPath_repeaterHops": "ホップの繰り返し",
"channelPath_repeaterHops": "リピータホップ",
"channelPath_noHopDetails": "このパッケージに関する詳細な情報は提供されていません。",
"channelPath_messageDetails": "メッセージの詳細",
"channelPath_senderLabel": "送信者",
"channelPath_timeLabel": "時間",
"channelPath_repeatsLabel": "繰り返し",
"channelPath_repeatsLabel": "リピータ",
"channelPath_pathLabel": "{index} 番目の経路",
"channelPath_observedLabel": "観察",
"channelPath_observedPathTitle": "観察された経路 {index} • {hops}",
@@ -1592,7 +1631,7 @@
}
},
"channelPath_unknownPath": "不明",
"channelPath_floodPath": "洪水",
"channelPath_floodPath": "フラッド",
"channelPath_directPath": "直接",
"channelPath_observedZeroOf": "{total}個のホップ",
"@channelPath_observedZeroOf": {
@@ -1614,7 +1653,7 @@
}
},
"channelPath_mapTitle": "経路図",
"channelPath_noRepeaterLocations": "この経路には、中継装置の設置場所がありません。",
"channelPath_noRepeaterLocations": "この経路にリピータの位置情報はありません。",
"channelPath_primaryPath": "{index}番目の経路(主要経路)",
"@channelPath_primaryPath": {
"placeholders": {
@@ -1792,7 +1831,7 @@
"listFilter_addToFavorites": "お気に入りに追加",
"listFilter_removeFromFavorites": "お気に入りから削除",
"listFilter_users": "利用者",
"listFilter_repeaters": "繰り返し送信装置",
"listFilter_repeaters": "リピータ",
"listFilter_roomServers": "ルーム用サーバー",
"listFilter_unreadOnly": "未読のみ",
"listFilter_newGroup": "新しいグループ",
@@ -1937,8 +1976,8 @@
},
"contacts_pathTrace": "経路追跡",
"contacts_ping": "パング",
"contacts_repeaterPathTrace": "リピータまでの経路を追跡する",
"contacts_repeaterPing": "PING 繰り返し",
"contacts_repeaterPathTrace": "リピータまでの経路を追跡する",
"contacts_repeaterPing": "リピータにPING",
"contacts_roomPathTrace": "部屋のサーバーへの経路を追跡する",
"contacts_roomPing": "ピンルーム用サーバー",
"contacts_chatTraceRoute": "経路の追跡ルート",
@@ -1955,7 +1994,7 @@
"contacts_contactImported": "連絡先が登録されました。",
"contacts_contactImportFailed": "連絡先のインポートに失敗しました。",
"contacts_zeroHopAdvert": "ゼロホップ広告",
"contacts_floodAdvert": "洪水に関する広告",
"contacts_floodAdvert": "フラッドに関する広告",
"contacts_copyAdvertToClipboard": "広告をクリップボードにコピー",
"contacts_addContactFromClipboard": "クリップボードから連絡先を追加する",
"contacts_ShareContact": "連絡先をクリップボードにコピー",
@@ -1998,7 +2037,7 @@
}
},
"notification_receivedNewMessage": "新しいメッセージを受信",
"settings_gpxExportRepeaters": "GPX へのエクスポート用リピータ/ルームサーバー",
"settings_gpxExportRepeaters": "GPX へのエクスポート用リピータ/ルームサーバー",
"settings_gpxExportRepeatersSubtitle": "GPXファイルに場所情報を付加した、レピーター/ルームサーバーのエクスポート",
"settings_gpxExportContacts": "GPX 形式へのエクスポート",
"settings_gpxExportContactsSubtitle": "GPXファイルに位置情報を保存して、他の人と共有する。",
@@ -2008,20 +2047,20 @@
"settings_gpxExportNoContacts": "エクスポートする連絡先は存在しません。",
"settings_gpxExportNotAvailable": "このデバイス/OSではサポートされていません",
"settings_gpxExportError": "エクスポート時にエラーが発生しました。",
"settings_gpxExportRepeatersRoom": "中継装置およびルームサーバーの設置場所",
"settings_gpxExportRepeatersRoom": "リピータ/ルームサーバーの位置情報",
"settings_gpxExportChat": "関連施設",
"settings_gpxExportAllContacts": "すべての連絡先場所",
"settings_gpxExportShareText": "meshcore-openからエクスポートされた地図データ",
"settings_gpxExportShareSubject": "meshcore-open GPX形式の地図データのエクスポート",
"snrIndicator_nearByRepeaters": "近くの電波中継局",
"snrIndicator_nearByRepeaters": "近くのリピータ",
"snrIndicator_lastSeen": "最後に確認された場所",
"contactsSettings_title": "連絡先設定",
"contactsSettings_autoAddTitle": "自動検出",
"contactsSettings_otherTitle": "その他の連絡に関する設定",
"contactsSettings_autoAddUsersTitle": "自動でユーザーを追加する",
"contactsSettings_autoAddUsersSubtitle": "利用者が自動的に発見したユーザーを追加できるようにする。",
"contactsSettings_autoAddRepeatersTitle": "自動で繰り返し設定",
"contactsSettings_autoAddRepeatersSubtitle": "発見した中継局を、自動的に追加できるようにする。",
"contactsSettings_autoAddRepeatersTitle": "リピータを自動追加",
"contactsSettings_autoAddRepeatersSubtitle": "発見したリピータを、自動的に追加できるようにする。",
"contactsSettings_autoAddRoomServersTitle": "自動でルームサーバーを追加",
"contactsSettings_autoAddRoomServersSubtitle": "利用者が、発見した部屋のサーバーを自動的に追加できるようにする。",
"contactsSettings_autoAddSensorsTitle": "自動でセンサーを追加",
@@ -2123,7 +2162,7 @@
"contact_teleLocSubtitle": "位置情報共有を許可する",
"contact_teleEnv": "テレメトリ環境",
"contact_teleEnvSubtitle": "環境センサーのデータを共有することを許可する",
"map_showOverlaps": "リピータキーの重複",
"map_showOverlaps": "リピータキーの重複",
"map_runTraceWithReturnPath": "元の経路に戻る。",
"@translation_downloadFailed": {
"placeholders": {
@@ -2193,7 +2232,7 @@
"repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。",
"room_guest": "ルームサーバーに関する情報",
"chat_sendMessage": "メッセージを送信する",
"repeater_guest": "繰り返し送信に関する情報",
"repeater_guest": "リピータに関する情報",
"repeater_guestTools": "ゲスト向けツール",
"repeater_getCategory": "価値を取得する",
"repeater_powerMgmt": "電力管理",
@@ -2204,7 +2243,7 @@
"repeater_cliHelpStartOta": "サポートされているボードに対して、無線でファームウェアのアップデートを開始します。",
"repeater_cliHelpTime": "デバイスのクロックを、指定されたUnixエポックの秒数に設定します。クロックは逆方向に進むことはできません。",
"repeater_cliHelpBoard": "製造元の名前/ハードウェア識別子を表示します。",
"repeater_cliHelpDiscoverNeighbors": "近隣のノードに対して、ノードの探索リクエストを送信します。(中継機能のみ)",
"repeater_cliHelpDiscoverNeighbors": "近隣のノードに対して、ノードの探索リクエストを送信します。(リピータ機能のみ)",
"repeater_cliHelpPowersaving": "省電力モードがオンになっているかどうかを表示します。",
"repeater_cliHelpPowersavingOnOff": "省電力モード(対応している場合)を有効または無効にします。",
"repeater_cliHelpErase": "(シリアルモードのみ)デバイスのファイルシステムをフォーマットします。すべての設定と連絡先を消去します。",
@@ -2217,10 +2256,10 @@
"repeater_cliHelpSetFreq": "(シリアル設定のみ)特定の周波数のみを素早く設定できます。再起動が必要です。「ラジオ設定」を使用すると、ラジオのすべてのパラメータを設定できます。",
"repeater_cliHelpSetBridgeChannel": "ESPNowブリッジのみ)ブリッジで使用するWi-Fiチャンネル(1~14)を設定します。",
"repeater_cliHelpGetName": "設定されたノードの名前を表示します。",
"repeater_cliHelpGetRole": "ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。",
"repeater_cliHelpGetRole": "ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。",
"repeater_cliHelpGetPublicKey": "デバイスの公開鍵を表示します。",
"repeater_cliHelpGetPrvKey": "(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。",
"repeater_cliHelpGetRepeat": "パケット転送(リピータ機能)が有効になっているかどうかを表示します。",
"repeater_cliHelpGetRepeat": "パケット転送(リピータ機能)が有効になっているかどうかを表示します。",
"repeater_cliHelpGetTx": "現在のTX(送信)電力のdBm値を表示します。",
"repeater_cliHelpGetFreq": "設定された無線周波数をMHzで表示します。",
"repeater_cliHelpGetRadio": "以下のすべての無線パラメータを表示: 周波数、帯域幅、スプレッドファクター、符号化レート。",
@@ -2232,18 +2271,18 @@
"repeater_cliHelpGetMultiAcks": "ダブルACKモードが有効 (1) か無効 (0) かを示す。",
"repeater_cliHelpGetAllowReadOnly": "ゲストによる読み取り専用アクセスが許可されているかどうかを示す。",
"repeater_cliHelpGetAdvertInterval": "ローカル広告の時間を分単位で表示します。",
"repeater_cliHelpGetFloodAdvertInterval": "洪水に関する広告の放送時間を時間単位で表示します。",
"repeater_cliHelpGetFloodAdvertInterval": "フラッドに関する広告の放送時間を時間単位で表示します。",
"repeater_cliHelpGetGuestPassword": "設定されたゲストパスワードを表示します。",
"repeater_cliHelpGetLat": "設定された緯度を表示します。",
"repeater_cliHelpGetLon": "設定された経度を表示します。",
"repeater_cliHelpGetRxDelay": "rxdelay の基本値を表示します。",
"repeater_cliHelpGetTxDelay": "洪水モードにおける送信遅延の要因を示します。",
"repeater_cliHelpGetTxDelay": "フラッドモードにおける送信遅延の要因を示します。",
"repeater_cliHelpGetDirectTxDelay": "ダイレクトモードの遅延要素を示します。",
"repeater_cliHelpGetFloodMax": "最大浸水範囲の回数を表示します。",
"repeater_cliHelpGetFloodMax": "フラッドパケットの最大ホップ数を表示します。",
"repeater_cliHelpGetOwnerInfo": "所有者の連絡先情報を表示します。",
"repeater_cliHelpGetPathHashMode": "パスハッシュモード(0/1/2)を表示します。",
"repeater_cliHelpGetLoopDetect": "ループ検出の感度を示す。",
"repeater_cliHelpGetAcl": "(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。",
"repeater_cliHelpGetAcl": "(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。",
"repeater_cliHelpGetBridgeEnabled": "橋が有効になっているかどうかを表示します。",
"repeater_cliHelpGetBridgeDelay": "橋の遅延時間をミリ秒(ms)で表示します。",
"repeater_cliHelpGetBridgeSource": "RX または TX パケットを橋渡ししているかどうかを示す。",
@@ -2261,8 +2300,8 @@
"repeater_cliHelpSensorList": "カスタムセンサーの設定をすべてリスト表示し、オプションで指定できる開始インデックスからページ分割して表示します。",
"repeater_cliHelpRegionDefault": "現在のデフォルトの地域範囲を表示します。",
"repeater_cliHelpRegionDefaultSet": "デフォルトの地域範囲を設定します。「<null>」を使用すると、設定をリセットできます。",
"repeater_cliHelpRegionListAllowed": "洪水時の通行が許可されている地域の一覧",
"repeater_cliHelpRegionListDenied": "洪水による交通を遮断している地域の一覧",
"repeater_cliHelpRegionListAllowed": "フラッド時の通行が許可されている地域の一覧",
"repeater_cliHelpRegionListDenied": "フラッドによる交通を遮断している地域の一覧",
"repeater_cliHelpStatsPackets": "(シリアルのみ)パケットレベルの統計情報を表示します。",
"repeater_cliHelpStatsRadio": "(シリーズのみ)ラジオの統計情報を表示します。",
"repeater_cliHelpStatsCore": "(シリアルのみ)主要なファームウェアの統計情報を表示します。",
@@ -2352,5 +2391,98 @@
"settings_companionDebugLog": "同伴デバッグログ",
"chat_newMessages": "新しいメッセージ",
"chat_markAsUnread": "未読としてマークする",
"repeater_chanUtil": "チャンネルの利用状況"
"repeater_chanUtil": "チャンネルの利用状況",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_delivered": "配達",
"common_undo": "元に戻す",
"messageStatus_pending": "送信",
"messageStatus_sent": "送信",
"messageStatus_failed": "送信できませんでした",
"contacts_moreOptions": "さらに多くの選択肢",
"messageStatus_repeated": "何度も聞いた",
"contacts_searchOpen": "連絡先を検索する",
"contacts_searchClose": "検索を終了",
"routing_modeFlood": "洪水",
"routing_title": "経路設定",
"routing_modeAuto": "自動車",
"routing_modeManual": "マニュアル",
"routing_modeAutoHint": "最も一般的な経路を自動的に選択し、経路が不明な場合は、水没状態にします。",
"routing_modeFloodHint": "すべてのリピーターを通じて放送。最も信頼性が高いですが、より多くの時間を使用します。",
"routing_modeManualHint": "常に、あなたが設定した正確な経路を辿って移動します。",
"routing_currentRoute": "現在までのルート",
"routing_directNoHops": "直接接続—中継装置を経由しない",
"routing_noPathYet": "まだ経路は確立されていません。「次のメッセージを送信し、経路が特定されるまで待ちます」。",
"routing_floodBroadcast": "すべてのリピーターを通じて放送",
"routing_editPath": "パスの編集",
"routing_forgetPath": "道にこだわらない",
"routing_knownPaths": "既知の経路",
"routing_knownPathsHint": "そのアプリケーションに切り替えるためのショートカットを作成します。",
"routing_inUse": "使用中",
"routing_qualityStrong": "最初の段階で大きな成果を上げる",
"routing_qualityGood": "最初の成功",
"routing_qualityFair": "最初の試みは成功を収めた",
"routing_qualityWorked": "完了しました",
"routing_qualityFlood": "氾濫によって伝聞",
"routing_qualityUntested": "未検証",
"routing_lastWorked": "{when}に勤務",
"routing_neverWorked": "確認されていない",
"routing_floodDelivery": "洪水による配送",
"pathEditor_title": "経路の作成",
"pathEditor_hopCounter": "64個のホップのうち、{count}個",
"pathEditor_noHops": "まだホップは追加されていません。ホップを順番に追加するには、以下の「タップ」ボタンをクリックしてください。または、ホップを一切追加せずに直接送信するには、「保存」ボタンをクリックしてください。",
"pathEditor_addHops": "ホップを、指定された順番に加える",
"pathEditor_searchRepeaters": "繰り返し検索",
"pathEditor_advancedHex": "高度なレベル:生のヘックスパス",
"pathEditor_hexLabel": "ヘックスプレフィックス",
"pathEditor_hexHelper": "各ホップごとに2つのハッシュ文字を、カンマで区切って記述",
"pathEditor_invalidTokens": "無効: {tokens}",
"pathEditor_tooManyHops": "最大64段階",
"pathEditor_usePath": "この経路を使用してください",
"pathEditor_removeHop": "ホップを取り除く",
"pathEditor_unknownHop": "不明な増幅器",
"map_zoomIn": "ズームイン",
"map_zoomOut": "ズームアウト",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_centerMap": "中心地図",
"chrome_bluetoothRequiresChromium": "Web Bluetooth は、Chromium ブラウザが必要です。",
"channels_communityShortId": "ID: {id}…",
"pathTrace_legendGpsConfirmed": "GPSによる確認",
"pathTrace_legendInferred": "推測される位置"
}
+133 -1
View File
@@ -27,6 +27,8 @@
"common_remove": "제거",
"common_enable": "활성화",
"common_disable": "비활성화",
"common_autoRefresh": "자동 새로고침",
"common_interval": "간격",
"common_reboot": "재부팅",
"common_loading": "로딩 중...",
"common_notAvailable": "—",
@@ -1466,6 +1468,43 @@
}
}
},
"telemetry_digitalInputLabel": "디지털 입력",
"telemetry_digitalOutputLabel": "디지털 출력",
"telemetry_analogInputLabel": "아날로그 입력",
"telemetry_analogOutputLabel": "아날로그 출력",
"telemetry_genericLabel": "일반 센서",
"telemetry_luminosityLabel": "조도",
"telemetry_presenceLabel": "존재 감지",
"telemetry_humidityLabel": "습도",
"telemetry_accelerometerLabel": "가속도계",
"telemetry_pressureLabel": "압력",
"telemetry_altitudeLabel": "고도",
"telemetry_frequencyLabel": "주파수",
"telemetry_percentageLabel": "백분율",
"telemetry_concentrationLabel": "농도",
"telemetry_powerLabel": "전력",
"telemetry_distanceLabel": "거리",
"telemetry_energyLabel": "에너지",
"telemetry_directionLabel": "방향",
"telemetry_timeLabel": "시간",
"telemetry_gyrometerLabel": "자이로미터",
"telemetry_colourLabel": "색상",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "스위치",
"telemetry_polylineLabel": "폴리라인",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "요청 수",
"telemetry_error": "데이터를 가져올 수 없습니다",
"telemetry_noData": "텔레메트리 데이터는 제공되지 않습니다.",
"telemetry_channelTitle": "채널 {channel}",
"@telemetry_channelTitle": {
@@ -2352,5 +2391,98 @@
"chat_newMessages": "새로운 메시지",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 명령어, 응답 및 원시 데이터",
"chat_markAsUnread": "미리 읽지 않음으로 표시",
"repeater_chanUtil": "채널 활용도"
"repeater_chanUtil": "채널 활용도",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_pending": "발송",
"messageStatus_sent": "발송",
"messageStatus_delivered": "배송 완료",
"common_undo": "취소",
"messageStatus_failed": "발송 실패",
"messageStatus_repeated": "반복적으로 들었습니다",
"contacts_searchOpen": "연락처 검색",
"contacts_moreOptions": "더 많은 옵션",
"contacts_searchClose": "검색 닫기",
"routing_title": "라우팅",
"routing_modeAuto": "자동",
"routing_modeFlood": "홍수",
"routing_modeManual": "사용 설명서",
"routing_modeAutoHint": "가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없을 경우에는 무작위로 경로를 선택합니다.",
"routing_modeFloodHint": "모든 증폭기를 통해 방송됩니다. 가장 안정적이지만, 더 많은 송출 시간을 사용합니다.",
"routing_modeManualHint": "항상 설정하신 정확한 경로를 따라 이동합니다.",
"routing_currentRoute": "현재 경로",
"routing_directNoHops": "직접 연결 중계 장치 사용 없이",
"routing_noPathYet": "아직 경로가 없습니다. 다음 메시지가 도착할 때까지 계속 탐색합니다.",
"routing_floodBroadcast": "모든 증폭기를 통해 방송",
"routing_editPath": "경로 편집",
"routing_forgetPath": "길을 잊어라",
"routing_knownPaths": "알려진 경로",
"routing_knownPathsHint": "해당 항목으로 전환하기 위한 경로를 선택합니다.",
"routing_inUse": "사용 중",
"routing_qualityStrong": "강력한 첫 번째 단계",
"routing_qualityGood": "좋은 첫 시작",
"routing_qualityFair": "처음 시도",
"routing_qualityWorked": "완료됨",
"routing_qualityFlood": "홍수 피해 상황을 통해 들었습니다.",
"routing_qualityUntested": "검증되지 않음",
"routing_lastWorked": "{when}에 일했습니다",
"routing_neverWorked": "확인되지 않음",
"routing_floodDelivery": "홍수 피해 지역 배송",
"pathEditor_title": "경로 만들기",
"pathEditor_hopCounter": "64개의 홉 중 {count}",
"pathEditor_noHops": "현재 홉은 추가되지 않았습니다. 아래의 탭을 사용하여 순서대로 추가하거나, 홉 없이 바로 전송하려면 \"홉 없음\"으로 저장하십시오.",
"pathEditor_addHops": "홉을 순서대로 첨가해주세요.",
"pathEditor_searchRepeaters": "반복 검색",
"pathEditor_advancedHex": "고급: 원시 헥스 경로",
"pathEditor_hexLabel": "헥스 접두사",
"pathEditor_hexHelper": "각 홉마다 2개의 6자리 숫자, 쉼표로 구분",
"pathEditor_invalidTokens": "유효하지 않음: {tokens}",
"pathEditor_tooManyHops": "최대 64개의 홉",
"pathEditor_usePath": "이 경로를 사용하세요",
"pathEditor_removeHop": "홉 제거",
"pathEditor_unknownHop": "알 수 없는 중계기",
"map_zoomIn": "줌 인",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "줌 아웃",
"map_centerMap": "중심 지도",
"chrome_bluetoothRequiresChromium": "웹 블루투스는 크롬 브라우저가 필요합니다.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS 확인 완료",
"pathTrace_legendInferred": "추론된 위치"
}
+558 -210
View File
@@ -298,6 +298,42 @@ abstract class AppLocalizations {
/// **'Disable'**
String get common_disable;
/// No description provided for @common_undo.
///
/// In en, this message translates to:
/// **'Undo'**
String get common_undo;
/// No description provided for @messageStatus_sent.
///
/// In en, this message translates to:
/// **'Sent'**
String get messageStatus_sent;
/// No description provided for @messageStatus_delivered.
///
/// In en, this message translates to:
/// **'Delivered'**
String get messageStatus_delivered;
/// No description provided for @messageStatus_pending.
///
/// In en, this message translates to:
/// **'Sending'**
String get messageStatus_pending;
/// No description provided for @messageStatus_failed.
///
/// In en, this message translates to:
/// **'Failed to send'**
String get messageStatus_failed;
/// No description provided for @messageStatus_repeated.
///
/// In en, this message translates to:
/// **'Heard repeated'**
String get messageStatus_repeated;
/// No description provided for @common_reboot.
///
/// In en, this message translates to:
@@ -328,6 +364,18 @@ abstract class AppLocalizations {
/// **'{percent}%'**
String common_percentValue(int percent);
/// No description provided for @common_autoRefresh.
///
/// In en, this message translates to:
/// **'Autorefresh'**
String get common_autoRefresh;
/// No description provided for @common_interval.
///
/// In en, this message translates to:
/// **'Interval'**
String get common_interval;
/// No description provided for @scanner_title.
///
/// In en, this message translates to:
@@ -1546,12 +1594,6 @@ abstract class AppLocalizations {
/// **'Number of retry attempts before marking a message as failed'**
String get appSettings_maxMessageRetriesSubtitle;
/// No description provided for @path_routeWeight.
///
/// In en, this message translates to:
/// **'{weight}/{max}'**
String path_routeWeight(String weight, String max);
/// No description provided for @appSettings_battery.
///
/// In en, this message translates to:
@@ -1894,6 +1936,24 @@ abstract class AppLocalizations {
/// **'New Group'**
String get contacts_newGroup;
/// No description provided for @contacts_moreOptions.
///
/// In en, this message translates to:
/// **'More options'**
String get contacts_moreOptions;
/// No description provided for @contacts_searchOpen.
///
/// In en, this message translates to:
/// **'Search contacts'**
String get contacts_searchOpen;
/// No description provided for @contacts_searchClose.
///
/// In en, this message translates to:
/// **'Close search'**
String get contacts_searchClose;
/// No description provided for @contacts_groupName.
///
/// In en, this message translates to:
@@ -2722,78 +2782,12 @@ abstract class AppLocalizations {
/// **'Hex Dump:'**
String get debugFrame_hexDump;
/// No description provided for @chat_pathManagement.
///
/// In en, this message translates to:
/// **'Path Management'**
String get chat_pathManagement;
/// No description provided for @chat_ShowAllPaths.
///
/// In en, this message translates to:
/// **'Show all paths'**
String get chat_ShowAllPaths;
/// No description provided for @chat_routingMode.
///
/// In en, this message translates to:
/// **'Routing mode'**
String get chat_routingMode;
/// No description provided for @chat_autoUseSavedPath.
///
/// In en, this message translates to:
/// **'Auto (use saved path)'**
String get chat_autoUseSavedPath;
/// No description provided for @chat_forceFloodMode.
///
/// In en, this message translates to:
/// **'Force Flood Mode'**
String get chat_forceFloodMode;
/// No description provided for @chat_recentAckPaths.
///
/// In en, this message translates to:
/// **'Recent ACK Paths (tap to use):'**
String get chat_recentAckPaths;
/// No description provided for @chat_pathHistoryFull.
///
/// In en, this message translates to:
/// **'Path history is full. Remove entries to add new ones.'**
String get chat_pathHistoryFull;
/// No description provided for @chat_hopSingular.
///
/// In en, this message translates to:
/// **'hop'**
String get chat_hopSingular;
/// No description provided for @chat_hopPlural.
///
/// In en, this message translates to:
/// **'hops'**
String get chat_hopPlural;
/// No description provided for @chat_hopsCount.
///
/// In en, this message translates to:
/// **'{count} {count, plural, =1{hop} other{hops}}'**
String chat_hopsCount(int count);
/// No description provided for @chat_successes.
///
/// In en, this message translates to:
/// **'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:
@@ -2806,71 +2800,251 @@ abstract class AppLocalizations {
/// **'No path history yet.\nSend a message to discover paths.'**
String get chat_noPathHistoryYet;
/// No description provided for @chat_pathActions.
///
/// In en, this message translates to:
/// **'Path Actions:'**
String get chat_pathActions;
/// No description provided for @chat_setCustomPath.
///
/// In en, this message translates to:
/// **'Set Custom Path'**
String get chat_setCustomPath;
/// No description provided for @chat_setCustomPathSubtitle.
///
/// In en, this message translates to:
/// **'Manually specify routing path'**
String get chat_setCustomPathSubtitle;
/// No description provided for @chat_clearPath.
///
/// In en, this message translates to:
/// **'Clear Path'**
String get chat_clearPath;
/// No description provided for @chat_clearPathSubtitle.
///
/// In en, this message translates to:
/// **'Force rediscovery on next send'**
String get chat_clearPathSubtitle;
/// No description provided for @chat_pathCleared.
///
/// In en, this message translates to:
/// **'Path cleared. Next message will rediscover route.'**
String get chat_pathCleared;
/// No description provided for @chat_floodModeSubtitle.
///
/// In en, this message translates to:
/// **'Use routing toggle in app bar'**
String get chat_floodModeSubtitle;
/// No description provided for @chat_floodModeEnabled.
///
/// In en, this message translates to:
/// **'Flood mode enabled. Toggle back via routing icon in app bar.'**
String get chat_floodModeEnabled;
/// No description provided for @chat_fullPath.
///
/// In en, this message translates to:
/// **'Full Path'**
String get chat_fullPath;
/// No description provided for @chat_pathDetailsNotAvailable.
/// No description provided for @routing_title.
///
/// In en, this message translates to:
/// **'Path details not available yet. Try sending a message to refresh.'**
String get chat_pathDetailsNotAvailable;
/// **'Routing'**
String get routing_title;
/// No description provided for @chat_pathSetHops.
/// No description provided for @routing_modeAuto.
///
/// In en, this message translates to:
/// **'Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}'**
String chat_pathSetHops(int hopCount, String status);
/// **'Auto'**
String get routing_modeAuto;
/// No description provided for @routing_modeFlood.
///
/// In en, this message translates to:
/// **'Flood'**
String get routing_modeFlood;
/// No description provided for @routing_modeManual.
///
/// In en, this message translates to:
/// **'Manual'**
String get routing_modeManual;
/// No description provided for @routing_modeAutoHint.
///
/// In en, this message translates to:
/// **'Picks the best known path automatically, flooding when none is known.'**
String get routing_modeAutoHint;
/// No description provided for @routing_modeFloodHint.
///
/// In en, this message translates to:
/// **'Broadcasts through every repeater. Most reliable, but uses more airtime.'**
String get routing_modeFloodHint;
/// No description provided for @routing_modeManualHint.
///
/// In en, this message translates to:
/// **'Always sends along the exact path you set.'**
String get routing_modeManualHint;
/// No description provided for @routing_currentRoute.
///
/// In en, this message translates to:
/// **'Current route'**
String get routing_currentRoute;
/// No description provided for @routing_directNoHops.
///
/// In en, this message translates to:
/// **'Direct — no repeater hops'**
String get routing_directNoHops;
/// No description provided for @routing_noPathYet.
///
/// In en, this message translates to:
/// **'No path yet. The next message floods until a route is discovered.'**
String get routing_noPathYet;
/// No description provided for @routing_floodBroadcast.
///
/// In en, this message translates to:
/// **'Broadcast through every repeater'**
String get routing_floodBroadcast;
/// No description provided for @routing_editPath.
///
/// In en, this message translates to:
/// **'Edit path'**
String get routing_editPath;
/// No description provided for @routing_forgetPath.
///
/// In en, this message translates to:
/// **'Forget path'**
String get routing_forgetPath;
/// No description provided for @routing_knownPaths.
///
/// In en, this message translates to:
/// **'Known paths'**
String get routing_knownPaths;
/// No description provided for @routing_knownPathsHint.
///
/// In en, this message translates to:
/// **'Tap a path to switch to it.'**
String get routing_knownPathsHint;
/// No description provided for @routing_inUse.
///
/// In en, this message translates to:
/// **'In use'**
String get routing_inUse;
/// No description provided for @routing_qualityStrong.
///
/// In en, this message translates to:
/// **'Strong first hop'**
String get routing_qualityStrong;
/// No description provided for @routing_qualityGood.
///
/// In en, this message translates to:
/// **'Good first hop'**
String get routing_qualityGood;
/// No description provided for @routing_qualityFair.
///
/// In en, this message translates to:
/// **'Fair first hop'**
String get routing_qualityFair;
/// No description provided for @routing_qualityWorked.
///
/// In en, this message translates to:
/// **'Has delivered'**
String get routing_qualityWorked;
/// No description provided for @routing_qualityFlood.
///
/// In en, this message translates to:
/// **'Heard via flood'**
String get routing_qualityFlood;
/// No description provided for @routing_qualityUntested.
///
/// In en, this message translates to:
/// **'Untested'**
String get routing_qualityUntested;
/// No description provided for @routing_lastWorked.
///
/// In en, this message translates to:
/// **'worked {when}'**
String routing_lastWorked(String when);
/// No description provided for @routing_neverWorked.
///
/// In en, this message translates to:
/// **'never confirmed'**
String get routing_neverWorked;
/// No description provided for @routing_deliveryCounts.
///
/// In en, this message translates to:
/// **'{successes} delivered, {failures} failed'**
String routing_deliveryCounts(int successes, int failures);
/// No description provided for @routing_floodDelivery.
///
/// In en, this message translates to:
/// **'Flood delivery'**
String get routing_floodDelivery;
/// No description provided for @pathEditor_title.
///
/// In en, this message translates to:
/// **'Build Path'**
String get pathEditor_title;
/// No description provided for @pathEditor_hopCounter.
///
/// In en, this message translates to:
/// **'{count} of 64 hops'**
String pathEditor_hopCounter(int count);
/// No description provided for @pathEditor_noHops.
///
/// In en, this message translates to:
/// **'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'**
String get pathEditor_noHops;
/// No description provided for @pathEditor_addHops.
///
/// In en, this message translates to:
/// **'Add hops in order'**
String get pathEditor_addHops;
/// No description provided for @pathEditor_searchRepeaters.
///
/// In en, this message translates to:
/// **'Search repeaters'**
String get pathEditor_searchRepeaters;
/// No description provided for @pathEditor_advancedHex.
///
/// In en, this message translates to:
/// **'Advanced: raw hex path'**
String get pathEditor_advancedHex;
/// No description provided for @pathEditor_hexLabel.
///
/// In en, this message translates to:
/// **'Hex prefixes'**
String get pathEditor_hexLabel;
/// No description provided for @pathEditor_hexHelper.
///
/// In en, this message translates to:
/// **'Two hex characters per hop, separated by commas'**
String get pathEditor_hexHelper;
/// No description provided for @pathEditor_invalidTokens.
///
/// In en, this message translates to:
/// **'Invalid: {tokens}'**
String pathEditor_invalidTokens(String tokens);
/// No description provided for @pathEditor_tooManyHops.
///
/// In en, this message translates to:
/// **'Maximum 64 hops'**
String get pathEditor_tooManyHops;
/// No description provided for @pathEditor_usePath.
///
/// In en, this message translates to:
/// **'Use this path'**
String get pathEditor_usePath;
/// No description provided for @pathEditor_removeHop.
///
/// In en, this message translates to:
/// **'Remove hop'**
String get pathEditor_removeHop;
/// No description provided for @pathEditor_unknownHop.
///
/// In en, this message translates to:
/// **'Unknown repeater'**
String get pathEditor_unknownHop;
/// No description provided for @chat_pathSavedLocally.
///
@@ -3699,90 +3873,18 @@ abstract class AppLocalizations {
/// **'Clear'**
String get common_clear;
/// No description provided for @path_currentPath.
///
/// In en, this message translates to:
/// **'Current path: {path}'**
String path_currentPath(String path);
/// No description provided for @path_usingHopsPath.
///
/// In en, this message translates to:
/// **'Using {count} {count, plural, =1{hop} other{hops}} path'**
String path_usingHopsPath(int count);
/// No description provided for @path_enterCustomPath.
///
/// In en, this message translates to:
/// **'Enter Custom Path'**
String get path_enterCustomPath;
/// No description provided for @path_currentPathLabel.
///
/// In en, this message translates to:
/// **'Current path'**
String get path_currentPathLabel;
/// No description provided for @path_hexPrefixInstructions.
///
/// In en, this message translates to:
/// **'Enter 2-character hex prefixes for each hop, separated by commas.'**
String get path_hexPrefixInstructions;
/// No description provided for @path_hexPrefixExample.
///
/// In en, this message translates to:
/// **'Example: A1,F2,3C (each node uses first byte of its public key)'**
String get path_hexPrefixExample;
/// No description provided for @path_labelHexPrefixes.
///
/// In en, this message translates to:
/// **'Path (hex prefixes)'**
String get path_labelHexPrefixes;
/// No description provided for @path_helperMaxHops.
///
/// In en, this message translates to:
/// **'Max 64 hops. Each prefix is 2 hex characters (1 byte)'**
String get path_helperMaxHops;
/// No description provided for @path_selectFromContacts.
///
/// In en, this message translates to:
/// **'Or select from contacts:'**
String get path_selectFromContacts;
/// No description provided for @path_noRepeatersFound.
///
/// In en, this message translates to:
/// **'No repeaters or room servers found.'**
String get path_noRepeatersFound;
/// No description provided for @path_customPathsRequire.
///
/// In en, this message translates to:
/// **'Custom paths require intermediate hops that can relay messages.'**
String get path_customPathsRequire;
/// No description provided for @path_invalidHexPrefixes.
///
/// In en, this message translates to:
/// **'Invalid hex prefixes: {prefixes}'**
String path_invalidHexPrefixes(String prefixes);
/// No description provided for @path_tooLong.
///
/// In en, this message translates to:
/// **'Path too long. Maximum 64 hops allowed.'**
String get path_tooLong;
/// No description provided for @path_setPath.
///
/// In en, this message translates to:
/// **'Set Path'**
String get path_setPath;
/// No description provided for @repeater_management.
///
/// In en, this message translates to:
@@ -3903,24 +4005,6 @@ abstract class AppLocalizations {
/// **'Routing mode'**
String get repeater_routingMode;
/// No description provided for @repeater_autoUseSavedPath.
///
/// In en, this message translates to:
/// **'Auto (use saved path)'**
String get repeater_autoUseSavedPath;
/// No description provided for @repeater_forceFloodMode.
///
/// In en, this message translates to:
/// **'Force Flood Mode'**
String get repeater_forceFloodMode;
/// No description provided for @repeater_pathManagement.
///
/// In en, this message translates to:
/// **'Path management'**
String get repeater_pathManagement;
/// No description provided for @repeater_refresh.
///
/// In en, this message translates to:
@@ -5714,6 +5798,228 @@ abstract class AppLocalizations {
/// **'{celsius}°C / {fahrenheit}°F'**
String telemetry_temperatureValue(String celsius, String fahrenheit);
/// No description provided for @telemetry_digitalInputLabel.
///
/// In en, this message translates to:
/// **'Digital Input'**
String get telemetry_digitalInputLabel;
/// No description provided for @telemetry_digitalOutputLabel.
///
/// In en, this message translates to:
/// **'Digital Output'**
String get telemetry_digitalOutputLabel;
/// No description provided for @telemetry_analogInputLabel.
///
/// In en, this message translates to:
/// **'Analog Input'**
String get telemetry_analogInputLabel;
/// No description provided for @telemetry_analogOutputLabel.
///
/// In en, this message translates to:
/// **'Analog Output'**
String get telemetry_analogOutputLabel;
/// No description provided for @telemetry_genericLabel.
///
/// In en, this message translates to:
/// **'Generic Sensor'**
String get telemetry_genericLabel;
/// No description provided for @telemetry_luminosityLabel.
///
/// In en, this message translates to:
/// **'Luminosity'**
String get telemetry_luminosityLabel;
/// No description provided for @telemetry_presenceLabel.
///
/// In en, this message translates to:
/// **'Presence'**
String get telemetry_presenceLabel;
/// No description provided for @telemetry_humidityLabel.
///
/// In en, this message translates to:
/// **'Humidity'**
String get telemetry_humidityLabel;
/// No description provided for @telemetry_accelerometerLabel.
///
/// In en, this message translates to:
/// **'Accelerometer'**
String get telemetry_accelerometerLabel;
/// No description provided for @telemetry_pressureLabel.
///
/// In en, this message translates to:
/// **'Pressure'**
String get telemetry_pressureLabel;
/// No description provided for @telemetry_altitudeLabel.
///
/// In en, this message translates to:
/// **'Altitude'**
String get telemetry_altitudeLabel;
/// No description provided for @telemetry_frequencyLabel.
///
/// In en, this message translates to:
/// **'Frequency'**
String get telemetry_frequencyLabel;
/// No description provided for @telemetry_percentageLabel.
///
/// In en, this message translates to:
/// **'Percentage'**
String get telemetry_percentageLabel;
/// No description provided for @telemetry_concentrationLabel.
///
/// In en, this message translates to:
/// **'Concentration'**
String get telemetry_concentrationLabel;
/// No description provided for @telemetry_powerLabel.
///
/// In en, this message translates to:
/// **'Power'**
String get telemetry_powerLabel;
/// No description provided for @telemetry_distanceLabel.
///
/// In en, this message translates to:
/// **'Distance'**
String get telemetry_distanceLabel;
/// No description provided for @telemetry_energyLabel.
///
/// In en, this message translates to:
/// **'Energy'**
String get telemetry_energyLabel;
/// No description provided for @telemetry_directionLabel.
///
/// In en, this message translates to:
/// **'Direction'**
String get telemetry_directionLabel;
/// No description provided for @telemetry_timeLabel.
///
/// In en, this message translates to:
/// **'Time'**
String get telemetry_timeLabel;
/// No description provided for @telemetry_gyrometerLabel.
///
/// In en, this message translates to:
/// **'Gyrometer'**
String get telemetry_gyrometerLabel;
/// No description provided for @telemetry_colourLabel.
///
/// In en, this message translates to:
/// **'Colour'**
String get telemetry_colourLabel;
/// No description provided for @telemetry_gpsLabel.
///
/// In en, this message translates to:
/// **'GPS'**
String get telemetry_gpsLabel;
/// No description provided for @telemetry_switchLabel.
///
/// In en, this message translates to:
/// **'Switch'**
String get telemetry_switchLabel;
/// No description provided for @telemetry_polylineLabel.
///
/// In en, this message translates to:
/// **'Polyline'**
String get telemetry_polylineLabel;
/// No description provided for @telemetry_altitudeValue.
///
/// In en, this message translates to:
/// **'{meters} m'**
String telemetry_altitudeValue(String meters);
/// No description provided for @telemetry_frequencyValue.
///
/// In en, this message translates to:
/// **'{hertz} Hz'**
String telemetry_frequencyValue(String hertz);
/// No description provided for @telemetry_pressureValue.
///
/// In en, this message translates to:
/// **'{hpa} hPa'**
String telemetry_pressureValue(String hpa);
/// No description provided for @telemetry_luminosityValue.
///
/// In en, this message translates to:
/// **'{lux} lx'**
String telemetry_luminosityValue(String lux);
/// No description provided for @telemetry_powerValue.
///
/// In en, this message translates to:
/// **'{watts} W'**
String telemetry_powerValue(String watts);
/// No description provided for @telemetry_distanceValue.
///
/// In en, this message translates to:
/// **'{meters} m'**
String telemetry_distanceValue(String meters);
/// No description provided for @telemetry_energyValue.
///
/// In en, this message translates to:
/// **'{kilowattHours} kWh'**
String telemetry_energyValue(String kilowattHours);
/// No description provided for @telemetry_directionValue.
///
/// In en, this message translates to:
/// **'{degrees}°'**
String telemetry_directionValue(String degrees);
/// No description provided for @telemetry_concentrationValue.
///
/// In en, this message translates to:
/// **'{ppm} ppm'**
String telemetry_concentrationValue(String ppm);
/// No description provided for @telemetry_percentageValue.
///
/// In en, this message translates to:
/// **'{percent}%'**
String telemetry_percentageValue(String percent);
/// No description provided for @telemetry_analogValue.
///
/// In en, this message translates to:
/// **'{value}'**
String telemetry_analogValue(String value);
/// No description provided for @telemetry_autoFetchQuantity.
///
/// In en, this message translates to:
/// **'Requests quantity'**
String get telemetry_autoFetchQuantity;
/// No description provided for @telemetry_error.
///
/// In en, this message translates to:
/// **'Unable to retrieve data'**
String get telemetry_error;
/// No description provided for @neighbors_receivedData.
///
/// In en, this message translates to:
@@ -7377,6 +7683,48 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Unknown'**
String get contact_typeUnknown;
/// No description provided for @map_zoomIn.
///
/// In en, this message translates to:
/// **'Zoom in'**
String get map_zoomIn;
/// No description provided for @map_zoomOut.
///
/// In en, this message translates to:
/// **'Zoom out'**
String get map_zoomOut;
/// No description provided for @map_centerMap.
///
/// In en, this message translates to:
/// **'Center map'**
String get map_centerMap;
/// No description provided for @chrome_bluetoothRequiresChromium.
///
/// In en, this message translates to:
/// **'Web Bluetooth requires a Chromium browser'**
String get chrome_bluetoothRequiresChromium;
/// No description provided for @channels_communityShortId.
///
/// In en, this message translates to:
/// **'ID: {id}...'**
String channels_communityShortId(String id);
/// No description provided for @pathTrace_legendGpsConfirmed.
///
/// In en, this message translates to:
/// **'GPS confirmed'**
String get pathTrace_legendGpsConfirmed;
/// No description provided for @pathTrace_legendInferred.
///
/// In en, this message translates to:
/// **'Inferred position'**
String get pathTrace_legendInferred;
}
class _AppLocalizationsDelegate
+318 -137
View File
@@ -92,6 +92,24 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_disable => 'Деактивирай';
@override
String get common_undo => 'Отмяни';
@override
String get messageStatus_sent => 'Изпратено';
@override
String get messageStatus_delivered => 'Доставен';
@override
String get messageStatus_pending => 'Изпращане';
@override
String get messageStatus_failed => 'Не успях да изпратя';
@override
String get messageStatus_repeated => 'Слушах го многократно';
@override
String get common_reboot => 'Рестартирай';
@@ -111,6 +129,12 @@ class AppLocalizationsBg extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Автоматично обновяване';
@override
String get common_interval => 'Интервал';
@override
String get scanner_title => 'MeshCore – Отворена версия';
@@ -802,11 +826,6 @@ class AppLocalizationsBg extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батерия';
@@ -1008,6 +1027,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_newGroup => 'Нова група';
@override
String get contacts_moreOptions => 'Повече възможности';
@override
String get contacts_searchOpen => 'Търсене на контакти';
@override
String get contacts_searchClose => 'Затвори търсене';
@override
String get contacts_groupName => 'Група';
@@ -1488,35 +1516,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Хексадесетичен Dump:';
@override
String get chat_pathManagement => 'Управление на пътища';
@override
String get chat_ShowAllPaths => 'Покажи всички пътища';
@override
String get chat_routingMode => 'Режим на маршрутизиране';
@override
String get chat_autoUseSavedPath => 'Автоматично (използвай запазения път)';
@override
String get chat_forceFloodMode => 'Принуди режим на наводняване';
@override
String get chat_recentAckPaths =>
'Неотдавни ACK пътища (докоснете, за да използвате):';
@override
String get chat_pathHistoryFull =>
'Историята на пътя е пълна. Премахнете записи, за да добавите нови.';
@override
String get chat_hopSingular => 'скочи';
@override
String get chat_hopPlural => 'скоци';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1528,12 +1527,6 @@ class AppLocalizationsBg extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Успехи';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Премахни пътя';
@@ -1541,52 +1534,146 @@ class AppLocalizationsBg extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.';
@override
String get chat_pathActions => 'Действия по пътя:';
@override
String get chat_setCustomPath => 'Задайте персонализиран път';
@override
String get chat_setCustomPathSubtitle => 'Ръчно укажете маршрутен път';
@override
String get chat_clearPath => 'Почисти Път';
@override
String get chat_clearPathSubtitle =>
'Принуди преоткриване при следващо изпращане';
@override
String get chat_pathCleared =>
'Пътят е почистен. Следващото съобщение ще открие маршрута отново.';
@override
String get chat_floodModeSubtitle =>
'Използвайте превключвателя за маршрутизиране в лентата на приложението.';
@override
String get chat_floodModeEnabled =>
'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.';
@override
String get chat_fullPath => 'Пълен път';
@override
String get chat_pathDetailsNotAvailable =>
'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.';
String get routing_title => 'Маршрутизиране';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Пътят е зададен: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Автомобил';
@override
String get routing_modeFlood => 'Наводнение';
@override
String get routing_modeManual => 'Ръководство';
@override
String get routing_modeAutoHint =>
'Автоматично избира най-известния път, като при липса на информация, използва стратегия за \"запълване\" на празните пространства.';
@override
String get routing_modeFloodHint =>
'Излъчване през всички ретранслатори. Най-надежният начин, но изисква повече време на въздуха.';
@override
String get routing_modeManualHint =>
'Винаги следва точно пътя, който сте определили.';
@override
String get routing_currentRoute => 'Текущ маршрут';
@override
String get routing_directNoHops => 'Директ – без превключватели';
@override
String get routing_noPathYet =>
'Все още няма път. Съобщението продължава да се изпраща, докато не бъде открит маршрут.';
@override
String get routing_floodBroadcast => 'Предаване през всички ретранслатори';
@override
String get routing_editPath => 'Редактиране на пътя';
@override
String get routing_forgetPath => 'Забравете за пътя';
@override
String get routing_knownPaths => 'Известни маршрути';
@override
String get routing_knownPathsHint =>
'Натиснете бутона, за да превключите към него.';
@override
String get routing_inUse => 'В експлоатация';
@override
String get routing_qualityStrong => 'Силен първи скок';
@override
String get routing_qualityGood => 'Добър първи опит';
@override
String get routing_qualityFair => 'Първият добър скок';
@override
String get routing_qualityWorked => 'Беше изпълнено/Доведено до край';
@override
String get routing_qualityFlood =>
'Получено чрез информация, разпространена в резултат на навод.';
@override
String get routing_qualityUntested => 'Не тестван';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'никога не е потвърдено';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Доставка при навод';
@override
String get pathEditor_title => 'Създаване на път';
@override
String pathEditor_hopCounter(int count) {
return '$count от 64 различни вида малц';
}
@override
String get pathEditor_noHops =>
'Все още няма добавени хмел. Можете да използвате бутоните по-долу, за да ги добавите по ред, или да запазите рецептата без хмел, за да я изпратите директно.';
@override
String get pathEditor_addHops => 'Добавете хмела в реда, в който е посочено.';
@override
String get pathEditor_searchRepeaters => 'Търсене на повтори';
@override
String get pathEditor_advancedHex => 'Разширено: необработен шестничен път';
@override
String get pathEditor_hexLabel => 'Префикси на шестнадесетична система';
@override
String get pathEditor_hexHelper =>
'Два шест-символни идентификатора на скок, разделени със запетаи';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Невалидно: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Максимум 64 крачета';
@override
String get pathEditor_usePath => 'Използвайте този маршрут.';
@override
String get pathEditor_removeHop => 'Премахнете хмела';
@override
String get pathEditor_unknownHop => 'Неизвестен репитер';
@override
String get chat_pathSavedLocally =>
'Запазено локално. Свържете се за синхронизиране.';
@@ -2056,66 +2143,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_clear => 'Изчисти';
@override
String path_currentPath(String path) {
return 'Текущ път: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Използване на $count $_temp0 път';
}
@override
String get path_enterCustomPath => 'Въведете персонализиран път';
@override
String get path_currentPathLabel => 'Текущ път';
@override
String get path_hexPrefixInstructions =>
'Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.';
@override
String get path_hexPrefixExample =>
'A1,F2,3C (всяка нода използва първия байт от публичния си ключ)';
@override
String get path_labelHexPrefixes => 'Пътеки (шестнадесетични префикси)';
@override
String get path_helperMaxHops =>
'Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).';
@override
String get path_selectFromContacts => 'Изберете от контакти:';
@override
String get path_noRepeatersFound =>
'Няма намерени репетитори или сървъри на стаи.';
@override
String get path_customPathsRequire =>
'Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Невалидни шестнадесетични префикси: $prefixes';
}
@override
String get path_tooLong =>
'Пътят е твърде дълъг. Максимум 64 скока са разрешени.';
@override
String get path_setPath => 'Задайте път';
@override
String get repeater_management => 'Управление на повторители';
@@ -2182,16 +2216,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_routingMode => 'Режим на маршрутизиране';
@override
String get repeater_autoUseSavedPath =>
'Автоматично (използвай запазения път)';
@override
String get repeater_forceFloodMode => 'Принуди режим на наводняване';
@override
String get repeater_pathManagement => 'Управление на пътища';
@override
String get repeater_refresh => 'Презареди';
@@ -3288,6 +3312,139 @@ class AppLocalizationsBg extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Цифров вход';
@override
String get telemetry_digitalOutputLabel => 'Цифров изход';
@override
String get telemetry_analogInputLabel => 'Аналогов вход';
@override
String get telemetry_analogOutputLabel => 'Аналогов изход';
@override
String get telemetry_genericLabel => 'Общ сензор';
@override
String get telemetry_luminosityLabel => 'Осветеност';
@override
String get telemetry_presenceLabel => 'Присъствие';
@override
String get telemetry_humidityLabel => 'Влажност';
@override
String get telemetry_accelerometerLabel => 'Акселерометър';
@override
String get telemetry_pressureLabel => 'Налягане';
@override
String get telemetry_altitudeLabel => 'Надморска височина';
@override
String get telemetry_frequencyLabel => 'Честота';
@override
String get telemetry_percentageLabel => 'Процент';
@override
String get telemetry_concentrationLabel => 'Концентрация';
@override
String get telemetry_powerLabel => 'Мощност';
@override
String get telemetry_distanceLabel => 'Разстояние';
@override
String get telemetry_energyLabel => 'Енергия';
@override
String get telemetry_directionLabel => 'Посока';
@override
String get telemetry_timeLabel => 'Време';
@override
String get telemetry_gyrometerLabel => 'Жироскоп';
@override
String get telemetry_colourLabel => 'Цвят';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Превключвател';
@override
String get telemetry_polylineLabel => 'Полилиния';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Брой заявки';
@override
String get telemetry_error => 'Неуспешно получаване на данни';
@override
String get neighbors_receivedData => 'Получени данни за съседи';
@@ -4310,4 +4467,28 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Увеличи';
@override
String get map_zoomOut => 'Приближете се по-малко';
@override
String get map_centerMap => 'Карта на центъра';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth изисква браузър, базиран на Chromium.';
@override
String channels_communityShortId(String id) {
return 'Идентификационен номер: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS потвърдено';
@override
String get pathTrace_legendInferred => 'Извлечена позиция';
}
+320 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get common_disable => 'Deaktivieren';
@override
String get common_undo => 'Rückgängig machen';
@override
String get messageStatus_sent => 'Gesendet';
@override
String get messageStatus_delivered => 'Geliefert';
@override
String get messageStatus_pending => 'Versenden';
@override
String get messageStatus_failed => 'Nicht gesendet';
@override
String get messageStatus_repeated => 'Wiederholt gehört';
@override
String get common_reboot => 'Neustart';
@@ -111,6 +129,12 @@ class AppLocalizationsDe extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatische Aktualisierung';
@override
String get common_interval => 'Intervall';
@override
String get scanner_title => 'MeshCore Open-Version';
@@ -798,11 +822,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Akku';
@@ -1004,6 +1023,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contacts_newGroup => 'Neue Gruppe';
@override
String get contacts_moreOptions => 'Weitere Optionen';
@override
String get contacts_searchOpen => 'Kontakte suchen';
@override
String get contacts_searchClose => 'Erweiterte Suche';
@override
String get contacts_groupName => 'Gruppenname';
@@ -1486,36 +1514,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Hex-Dump:';
@override
String get chat_pathManagement => 'Pfadverwaltung';
@override
String get chat_ShowAllPaths => 'Alle Pfade anzeigen';
@override
String get chat_routingMode => 'Routenmodus';
@override
String get chat_autoUseSavedPath =>
'Automatisch (gespeicherten Pfad verwenden)';
@override
String get chat_forceFloodMode => 'Flut-Modus erzwingen';
@override
String get chat_recentAckPaths =>
'Aktuelle ACK-Pfade (antippen, um zu verwenden):';
@override
String get chat_pathHistoryFull =>
'Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.';
@override
String get chat_hopSingular => 'Sprung';
@override
String get chat_hopPlural => 'Sprünge';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1527,12 +1525,6 @@ class AppLocalizationsDe extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Erfolgreich';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Pfad entfernen';
@@ -1540,51 +1532,148 @@ class AppLocalizationsDe extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
@override
String get chat_pathActions => 'Pfadaktionen:';
@override
String get chat_setCustomPath => 'Lege benutzerdefinierten Pfad fest';
@override
String get chat_setCustomPathSubtitle => 'Manuellen Routenpfad festlegen';
@override
String get chat_clearPath => 'Pfad zurücksetzen';
@override
String get chat_clearPathSubtitle =>
'Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.';
@override
String get chat_pathCleared =>
'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.';
@override
String get chat_floodModeSubtitle =>
'Verwende den Routingschalter in der App-Leiste';
@override
String get chat_floodModeEnabled => 'Flutmodus aktiviert.';
@override
String get chat_fullPath => 'Vollständiger Pfad';
@override
String get chat_pathDetailsNotAvailable =>
'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.';
String get routing_title => 'Routenplanung';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Pfad gesetzt: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Auto';
@override
String get routing_modeFlood => 'Überschwemmung';
@override
String get routing_modeManual => 'Handbuch';
@override
String get routing_modeAutoHint =>
'Wählt automatisch den bekanntesten Pfad aus und verwendet eine Flutungsmethode, wenn kein Pfad bekannt ist.';
@override
String get routing_modeFloodHint =>
'Übertragung über alle Repeater. Die zuverlässigste Methode, jedoch mit höherem Datenverbrauch.';
@override
String get routing_modeManualHint =>
'Sendet immer genau den von Ihnen festgelegten Weg.';
@override
String get routing_currentRoute => 'Aktuelle Route';
@override
String get routing_directNoHops => 'Direkt ohne Zwischenverstärkung';
@override
String get routing_noPathYet =>
'Noch kein Pfad gefunden. Die Nachricht wird gesendet, bis ein Weg entdeckt wurde.';
@override
String get routing_floodBroadcast => 'Übertragung über jeden Repeater';
@override
String get routing_editPath => 'Pfad bearbeiten';
@override
String get routing_forgetPath => 'Vergiss den Weg';
@override
String get routing_knownPaths => 'Bekannte Routen';
@override
String get routing_knownPathsHint =>
'Wählen Sie den Pfad, um zu diesem zu wechseln.';
@override
String get routing_inUse => 'Im Gebrauch';
@override
String get routing_qualityStrong => 'Ein starker erster Sprung';
@override
String get routing_qualityGood => 'Ein guter erster Schritt';
@override
String get routing_qualityFair => 'Erster erfolgreicher Schritt';
@override
String get routing_qualityWorked => 'Hat erfolgreich geliefert';
@override
String get routing_qualityFlood =>
'Information erhalten durch Nachrichten über die Überschwemmung';
@override
String get routing_qualityUntested => 'Nicht getestet';
@override
String routing_lastWorked(String when) {
return 'war beschäftigt $when';
}
@override
String get routing_neverWorked => 'nie bestätigt';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Lieferung bei Überschwemmung';
@override
String get pathEditor_title => 'Pfad erstellen';
@override
String pathEditor_hopCounter(int count) {
return '$count von 64 Hopfengewächsen';
}
@override
String get pathEditor_noHops =>
'Noch keine Hopfen hinzugefügt. Klicken Sie auf die Schaltflächen unten, um sie nacheinander hinzuzufügen, oder speichern Sie die Rezepter ohne Hopfen, um sie direkt zu versenden.';
@override
String get pathEditor_addHops =>
'Fügen Sie die Hopfen in der richtigen Reihenfolge hinzu.';
@override
String get pathEditor_searchRepeaters =>
'Suche nach wiederholten Nachrichten';
@override
String get pathEditor_advancedHex => 'Fortgeschritten: Roh-Hex-Pfad';
@override
String get pathEditor_hexLabel => 'Hex-Präfixe';
@override
String get pathEditor_hexHelper =>
'Zwei Hexadezimalzeichen pro Sprung, getrennt durch Kommas';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Ungültig: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Maximal 64 Hopfengreifer';
@override
String get pathEditor_usePath => 'Verwenden Sie diesen Pfad.';
@override
String get pathEditor_removeHop => 'Hop entfernen';
@override
String get pathEditor_unknownHop => 'Unbekannter Repeater';
@override
String get chat_pathSavedLocally =>
'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.';
@@ -2055,65 +2144,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get common_clear => 'Löschen';
@override
String path_currentPath(String path) {
return 'Aktiver Pfad: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Hops',
one: 'Hop',
);
return 'Verwenden Sie $count $_temp0 Pfad';
}
@override
String get path_enterCustomPath => 'Gebe Pfad ein';
@override
String get path_currentPathLabel => 'Aktueller Pfad';
@override
String get path_hexPrefixInstructions =>
'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.';
@override
String get path_hexPrefixExample =>
'Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)';
@override
String get path_labelHexPrefixes => 'Pfad (Hex-Präfixe)';
@override
String get path_helperMaxHops =>
'Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)';
@override
String get path_selectFromContacts => 'Oder wähle aus Kontakten aus:';
@override
String get path_noRepeatersFound =>
'Keine Repeater oder Raumserver gefunden.';
@override
String get path_customPathsRequire =>
'Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Ungültige Hexadezimal-Präfixe: $prefixes';
}
@override
String get path_tooLong => 'Pfad zu lang. Maximal 64 Hops erlaubt.';
@override
String get path_setPath => 'Pfad festlegen';
@override
String get repeater_management => 'Repeater-Verwaltung';
@@ -2178,16 +2215,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_routingMode => 'Routenmodus';
@override
String get repeater_autoUseSavedPath =>
'Automatisch (gespeicherten Pfad verwenden)';
@override
String get repeater_forceFloodMode => 'Flut-Modus erzwingen';
@override
String get repeater_pathManagement => 'Pfadverwaltung';
@override
String get repeater_refresh => 'Aktualisieren';
@@ -3294,6 +3321,139 @@ class AppLocalizationsDe extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Digitaleingang';
@override
String get telemetry_digitalOutputLabel => 'Digitalausgang';
@override
String get telemetry_analogInputLabel => 'Analogeingang';
@override
String get telemetry_analogOutputLabel => 'Analogausgang';
@override
String get telemetry_genericLabel => 'Allgemeiner Sensor';
@override
String get telemetry_luminosityLabel => 'Helligkeit';
@override
String get telemetry_presenceLabel => 'Anwesenheit';
@override
String get telemetry_humidityLabel => 'Luftfeuchtigkeit';
@override
String get telemetry_accelerometerLabel => 'Beschleunigungsmesser';
@override
String get telemetry_pressureLabel => 'Druck';
@override
String get telemetry_altitudeLabel => 'Höhe';
@override
String get telemetry_frequencyLabel => 'Frequenz';
@override
String get telemetry_percentageLabel => 'Prozentsatz';
@override
String get telemetry_concentrationLabel => 'Konzentration';
@override
String get telemetry_powerLabel => 'Leistung';
@override
String get telemetry_distanceLabel => 'Entfernung';
@override
String get telemetry_energyLabel => 'Energie';
@override
String get telemetry_directionLabel => 'Richtung';
@override
String get telemetry_timeLabel => 'Zeit';
@override
String get telemetry_gyrometerLabel => 'Gyroskop';
@override
String get telemetry_colourLabel => 'Farbe';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Schalter';
@override
String get telemetry_polylineLabel => 'Polylinie';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Anzahl der Anfragen';
@override
String get telemetry_error => 'Daten konnten nicht abgerufen werden';
@override
String get neighbors_receivedData => 'Empfangene Nachbarsdaten';
@@ -4328,4 +4488,28 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Zoomen';
@override
String get map_zoomOut => 'Auszoomen';
@override
String get map_centerMap => 'Zentralkarte';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth benötigt einen Chromium-Browser.';
@override
String channels_communityShortId(String id) {
return 'ID: $id';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS-Bestätigung';
@override
String get pathTrace_legendInferred => 'Abgeleitete Position';
}
+316 -132
View File
@@ -92,6 +92,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_disable => 'Disable';
@override
String get common_undo => 'Undo';
@override
String get messageStatus_sent => 'Sent';
@override
String get messageStatus_delivered => 'Delivered';
@override
String get messageStatus_pending => 'Sending';
@override
String get messageStatus_failed => 'Failed to send';
@override
String get messageStatus_repeated => 'Heard repeated';
@override
String get common_reboot => 'Reboot';
@@ -111,6 +129,12 @@ class AppLocalizationsEn extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Autorefresh';
@override
String get common_interval => 'Interval';
@override
String get scanner_title => 'MeshCore Open';
@@ -783,11 +807,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Number of retry attempts before marking a message as failed';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Battery';
@@ -987,6 +1006,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contacts_newGroup => 'New Group';
@override
String get contacts_moreOptions => 'More options';
@override
String get contacts_searchOpen => 'Search contacts';
@override
String get contacts_searchClose => 'Close search';
@override
String get contacts_groupName => 'Group name';
@@ -1458,34 +1486,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Hex Dump:';
@override
String get chat_pathManagement => 'Path Management';
@override
String get chat_ShowAllPaths => 'Show all paths';
@override
String get chat_routingMode => 'Routing mode';
@override
String get chat_autoUseSavedPath => 'Auto (use saved path)';
@override
String get chat_forceFloodMode => 'Force Flood Mode';
@override
String get chat_recentAckPaths => 'Recent ACK Paths (tap to use):';
@override
String get chat_pathHistoryFull =>
'Path history is full. Remove entries to add new ones.';
@override
String get chat_hopSingular => 'hop';
@override
String get chat_hopPlural => 'hops';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1497,12 +1497,6 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'successes';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Remove path';
@@ -1510,50 +1504,144 @@ class AppLocalizationsEn extends AppLocalizations {
String get chat_noPathHistoryYet =>
'No path history yet.\nSend a message to discover paths.';
@override
String get chat_pathActions => 'Path Actions:';
@override
String get chat_setCustomPath => 'Set Custom Path';
@override
String get chat_setCustomPathSubtitle => 'Manually specify routing path';
@override
String get chat_clearPath => 'Clear Path';
@override
String get chat_clearPathSubtitle => 'Force rediscovery on next send';
@override
String get chat_pathCleared =>
'Path cleared. Next message will rediscover route.';
@override
String get chat_floodModeSubtitle => 'Use routing toggle in app bar';
@override
String get chat_floodModeEnabled =>
'Flood mode enabled. Toggle back via routing icon in app bar.';
@override
String get chat_fullPath => 'Full Path';
@override
String get chat_pathDetailsNotAvailable =>
'Path details not available yet. Try sending a message to refresh.';
String get routing_title => 'Routing';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Path set: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Auto';
@override
String get routing_modeFlood => 'Flood';
@override
String get routing_modeManual => 'Manual';
@override
String get routing_modeAutoHint =>
'Picks the best known path automatically, flooding when none is known.';
@override
String get routing_modeFloodHint =>
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
@override
String get routing_modeManualHint =>
'Always sends along the exact path you set.';
@override
String get routing_currentRoute => 'Current route';
@override
String get routing_directNoHops => 'Direct — no repeater hops';
@override
String get routing_noPathYet =>
'No path yet. The next message floods until a route is discovered.';
@override
String get routing_floodBroadcast => 'Broadcast through every repeater';
@override
String get routing_editPath => 'Edit path';
@override
String get routing_forgetPath => 'Forget path';
@override
String get routing_knownPaths => 'Known paths';
@override
String get routing_knownPathsHint => 'Tap a path to switch to it.';
@override
String get routing_inUse => 'In use';
@override
String get routing_qualityStrong => 'Strong first hop';
@override
String get routing_qualityGood => 'Good first hop';
@override
String get routing_qualityFair => 'Fair first hop';
@override
String get routing_qualityWorked => 'Has delivered';
@override
String get routing_qualityFlood => 'Heard via flood';
@override
String get routing_qualityUntested => 'Untested';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'never confirmed';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Flood delivery';
@override
String get pathEditor_title => 'Build Path';
@override
String pathEditor_hopCounter(int count) {
return '$count of 64 hops';
}
@override
String get pathEditor_noHops =>
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
@override
String get pathEditor_addHops => 'Add hops in order';
@override
String get pathEditor_searchRepeaters => 'Search repeaters';
@override
String get pathEditor_advancedHex => 'Advanced: raw hex path';
@override
String get pathEditor_hexLabel => 'Hex prefixes';
@override
String get pathEditor_hexHelper =>
'Two hex characters per hop, separated by commas';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Invalid: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Maximum 64 hops';
@override
String get pathEditor_usePath => 'Use this path';
@override
String get pathEditor_removeHop => 'Remove hop';
@override
String get pathEditor_unknownHop => 'Unknown repeater';
@override
String get chat_pathSavedLocally => 'Saved locally. Connect to sync.';
@@ -2015,64 +2103,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_clear => 'Clear';
@override
String path_currentPath(String path) {
return 'Current path: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Using $count $_temp0 path';
}
@override
String get path_enterCustomPath => 'Enter Custom Path';
@override
String get path_currentPathLabel => 'Current path';
@override
String get path_hexPrefixInstructions =>
'Enter 2-character hex prefixes for each hop, separated by commas.';
@override
String get path_hexPrefixExample =>
'Example: A1,F2,3C (each node uses first byte of its public key)';
@override
String get path_labelHexPrefixes => 'Path (hex prefixes)';
@override
String get path_helperMaxHops =>
'Max 64 hops. Each prefix is 2 hex characters (1 byte)';
@override
String get path_selectFromContacts => 'Or select from contacts:';
@override
String get path_noRepeatersFound => 'No repeaters or room servers found.';
@override
String get path_customPathsRequire =>
'Custom paths require intermediate hops that can relay messages.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Invalid hex prefixes: $prefixes';
}
@override
String get path_tooLong => 'Path too long. Maximum 64 hops allowed.';
@override
String get path_setPath => 'Set Path';
@override
String get repeater_management => 'Repeater Management';
@@ -2136,15 +2172,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_routingMode => 'Routing mode';
@override
String get repeater_autoUseSavedPath => 'Auto (use saved path)';
@override
String get repeater_forceFloodMode => 'Force Flood Mode';
@override
String get repeater_pathManagement => 'Path management';
@override
String get repeater_refresh => 'Refresh';
@@ -3225,6 +3252,139 @@ class AppLocalizationsEn extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Digital Input';
@override
String get telemetry_digitalOutputLabel => 'Digital Output';
@override
String get telemetry_analogInputLabel => 'Analog Input';
@override
String get telemetry_analogOutputLabel => 'Analog Output';
@override
String get telemetry_genericLabel => 'Generic Sensor';
@override
String get telemetry_luminosityLabel => 'Luminosity';
@override
String get telemetry_presenceLabel => 'Presence';
@override
String get telemetry_humidityLabel => 'Humidity';
@override
String get telemetry_accelerometerLabel => 'Accelerometer';
@override
String get telemetry_pressureLabel => 'Pressure';
@override
String get telemetry_altitudeLabel => 'Altitude';
@override
String get telemetry_frequencyLabel => 'Frequency';
@override
String get telemetry_percentageLabel => 'Percentage';
@override
String get telemetry_concentrationLabel => 'Concentration';
@override
String get telemetry_powerLabel => 'Power';
@override
String get telemetry_distanceLabel => 'Distance';
@override
String get telemetry_energyLabel => 'Energy';
@override
String get telemetry_directionLabel => 'Direction';
@override
String get telemetry_timeLabel => 'Time';
@override
String get telemetry_gyrometerLabel => 'Gyrometer';
@override
String get telemetry_colourLabel => 'Colour';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Switch';
@override
String get telemetry_polylineLabel => 'Polyline';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Requests quantity';
@override
String get telemetry_error => 'Unable to retrieve data';
@override
String get neighbors_receivedData => 'Received Neighbors Data';
@@ -4233,4 +4393,28 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Zoom in';
@override
String get map_zoomOut => 'Zoom out';
@override
String get map_centerMap => 'Center map';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth requires a Chromium browser';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
@override
String get pathTrace_legendInferred => 'Inferred position';
}
+319 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_disable => 'Desactivar';
@override
String get common_undo => 'Deshacer';
@override
String get messageStatus_sent => 'Sentido';
@override
String get messageStatus_delivered => 'Entregado';
@override
String get messageStatus_pending => 'Enviar';
@override
String get messageStatus_failed => 'No se pudo enviar';
@override
String get messageStatus_repeated => 'Escuché repetidamente';
@override
String get common_reboot => 'Reiniciar';
@@ -111,6 +129,12 @@ class AppLocalizationsEs extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Actualización automática';
@override
String get common_interval => 'Intervalo';
@override
String get scanner_title => 'MeshCore: Versión abierta';
@@ -797,11 +821,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Número de intentos de reintento antes de marcar un mensaje como fallido.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batería';
@@ -1003,6 +1022,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_newGroup => 'Nuevo Grupo';
@override
String get contacts_moreOptions => 'Más opciones';
@override
String get contacts_searchOpen => 'Buscar contactos';
@override
String get contacts_searchClose => 'Búsqueda avanzada';
@override
String get contacts_groupName => 'Nombre del grupo';
@@ -1485,34 +1513,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Mapeo Hexadecimal:';
@override
String get chat_pathManagement => 'Gestión de Rutas';
@override
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
@override
String get chat_routingMode => 'Modo de enrutamiento';
@override
String get chat_autoUseSavedPath => 'Auto (usar la ruta guardada)';
@override
String get chat_forceFloodMode => 'Modo Inundación Forzado';
@override
String get chat_recentAckPaths => 'Rutas de ACK Recientes (tocar para usar):';
@override
String get chat_pathHistoryFull =>
'El historial de rutas está completo. Eliminar entradas para añadir nuevas.';
@override
String get chat_hopSingular => 'salta';
@override
String get chat_hopPlural => 'salta';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1524,12 +1524,6 @@ class AppLocalizationsEs extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Éxitos';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Eliminar ruta';
@@ -1537,53 +1531,147 @@ class AppLocalizationsEs extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.';
@override
String get chat_pathActions => 'Acciones de Ruta:';
@override
String get chat_setCustomPath => 'Establecer Ruta Personalizada';
@override
String get chat_setCustomPathSubtitle =>
'Especificar manualmente la ruta de enrutamiento';
@override
String get chat_clearPath => 'Limpiar Ruta';
@override
String get chat_clearPathSubtitle =>
'Forzar redescubrimiento en el próximo envío';
@override
String get chat_pathCleared =>
'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.';
@override
String get chat_floodModeSubtitle =>
'Utilizar el interruptor de enrutamiento en la barra de herramientas';
@override
String get chat_floodModeEnabled =>
'El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.';
@override
String get chat_fullPath => 'Ruta completa';
@override
String get chat_pathDetailsNotAvailable =>
'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.';
String get routing_title => 'Ruteo';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Ruta establecida: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Coche';
@override
String get routing_modeFlood => 'Inundación';
@override
String get routing_modeManual => 'Manual';
@override
String get routing_modeAutoHint =>
'Selecciona automáticamente la ruta más conocida, y si no hay ninguna ruta conocida, utiliza la ruta más directa.';
@override
String get routing_modeFloodHint =>
'Transmisiones a través de todos los repetidores. Es la opción más fiable, pero utiliza más tiempo de transmisión.';
@override
String get routing_modeManualHint =>
'Siempre sigue exactamente la ruta que usted ha definido.';
@override
String get routing_currentRoute => 'Ruta actual';
@override
String get routing_directNoHops => 'Directo — sin saltos de repetidor';
@override
String get routing_noPathYet =>
'Aún no hay un camino definido. El mensaje se envía continuamente hasta que se encuentre una ruta.';
@override
String get routing_floodBroadcast =>
'Transmisión a través de todos los repetidores.';
@override
String get routing_editPath => 'Editar ruta';
@override
String get routing_forgetPath => 'Olvídate del camino';
@override
String get routing_knownPaths => 'Rutas conocidas';
@override
String get routing_knownPathsHint =>
'Seleccione una opción para cambiar a esa.';
@override
String get routing_inUse => 'En uso';
@override
String get routing_qualityStrong => 'Primer salto exitoso';
@override
String get routing_qualityGood => 'Primer paso exitoso';
@override
String get routing_qualityFair => 'Primer salto de calidad';
@override
String get routing_qualityWorked => 'Ha cumplido';
@override
String get routing_qualityFlood => 'Se ha escuchado a través de rumores.';
@override
String get routing_qualityUntested => 'Sin probar';
@override
String routing_lastWorked(String when) {
return 'trabajó $when';
}
@override
String get routing_neverWorked => 'nunca confirmado';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Entrega por inundación';
@override
String get pathEditor_title => 'Crear ruta';
@override
String pathEditor_hopCounter(int count) {
return '$count de 64 granos de lúpulo';
}
@override
String get pathEditor_noHops =>
'Aún no se han añadido lúpulos. Haga clic en los repetidores para añadirlos en el orden deseado, o guarde la receta sin lúpulos para enviarla directamente.';
@override
String get pathEditor_addHops => 'Añadir los lúpulos en el orden adecuado.';
@override
String get pathEditor_searchRepeaters => 'Buscar repetidores';
@override
String get pathEditor_advancedHex =>
'Avanzado: ruta hexadecimal sin procesar';
@override
String get pathEditor_hexLabel => 'Prefijos hexadecimales';
@override
String get pathEditor_hexHelper =>
'Dos caracteres hexadecimales por salto, separados por comas.';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Inválido: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Máximo 64 saltos';
@override
String get pathEditor_usePath => 'Utilice esta ruta.';
@override
String get pathEditor_removeHop => 'Eliminar el lúpulo';
@override
String get pathEditor_unknownHop => 'Repetidor desconocido';
@override
String get chat_pathSavedLocally =>
'Guardado localmente. Conéctate para sincronizar.';
@@ -2051,66 +2139,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_clear => 'Borrar';
@override
String path_currentPath(String path) {
return 'Ruta actual: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Usando $count $_temp0 ruta';
}
@override
String get path_enterCustomPath => 'Introducir Ruta Personalizada';
@override
String get path_currentPathLabel => 'Ruta actual';
@override
String get path_hexPrefixInstructions =>
'Introduzca los prefijos hexadecimales de 2 caracteres para cada salto, separados por comas.';
@override
String get path_hexPrefixExample =>
'Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).';
@override
String get path_labelHexPrefixes => 'Prefijos hexadecimales';
@override
String get path_helperMaxHops =>
'Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).';
@override
String get path_selectFromContacts => 'O seleccionar de contactos:';
@override
String get path_noRepeatersFound =>
'No se encontraron repetidores ni servidores de sala.';
@override
String get path_customPathsRequire =>
'Las rutas personalizadas requieren saltos intermedios que pueden transmitir mensajes.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Prefijos hexadecimales inválidos: $prefixes';
}
@override
String get path_tooLong =>
'La ruta es demasiado larga. Se permiten un máximo de 64 saltos.';
@override
String get path_setPath => 'Establecer Ruta';
@override
String get repeater_management => 'Gestión de Repetidores';
@@ -2175,15 +2210,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_routingMode => 'Modo de enrutamiento';
@override
String get repeater_autoUseSavedPath => 'Auto (usar la ruta guardada)';
@override
String get repeater_forceFloodMode => 'Modo Inundación Forzado';
@override
String get repeater_pathManagement => 'Gestión de rutas';
@override
String get repeater_refresh => 'Actualizar';
@@ -3282,6 +3308,139 @@ class AppLocalizationsEs extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Entrada digital';
@override
String get telemetry_digitalOutputLabel => 'Salida digital';
@override
String get telemetry_analogInputLabel => 'Entrada analógica';
@override
String get telemetry_analogOutputLabel => 'Salida analógica';
@override
String get telemetry_genericLabel => 'Sensor genérico';
@override
String get telemetry_luminosityLabel => 'Luminosidad';
@override
String get telemetry_presenceLabel => 'Presencia';
@override
String get telemetry_humidityLabel => 'Humedad';
@override
String get telemetry_accelerometerLabel => 'Acelerómetro';
@override
String get telemetry_pressureLabel => 'Presión';
@override
String get telemetry_altitudeLabel => 'Altitud';
@override
String get telemetry_frequencyLabel => 'Frecuencia';
@override
String get telemetry_percentageLabel => 'Porcentaje';
@override
String get telemetry_concentrationLabel => 'Concentración';
@override
String get telemetry_powerLabel => 'Potencia';
@override
String get telemetry_distanceLabel => 'Distancia';
@override
String get telemetry_energyLabel => 'Energía';
@override
String get telemetry_directionLabel => 'Dirección';
@override
String get telemetry_timeLabel => 'Hora';
@override
String get telemetry_gyrometerLabel => 'Girómetro';
@override
String get telemetry_colourLabel => 'Color';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Interruptor';
@override
String get telemetry_polylineLabel => 'Polilínea';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Número de solicitudes';
@override
String get telemetry_error => 'No se pudieron obtener los datos';
@override
String get neighbors_receivedData => 'Recibidas Datos de Vecinos';
@@ -4315,4 +4474,28 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Acercar';
@override
String get map_zoomOut => 'Acercar';
@override
String get map_centerMap => 'Mapa del centro';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth requiere un navegador Chromium.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'Confirmado mediante GPS';
@override
String get pathTrace_legendInferred => 'Posición inferida';
}
+318 -138
View File
@@ -92,6 +92,24 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_disable => 'Désactiver';
@override
String get common_undo => 'Annuler';
@override
String get messageStatus_sent => 'Envoyer';
@override
String get messageStatus_delivered => 'Livré';
@override
String get messageStatus_pending => 'Envoyer';
@override
String get messageStatus_failed => 'Échec de l\'envoi';
@override
String get messageStatus_repeated => 'Répété plusieurs fois';
@override
String get common_reboot => 'Redémarrer';
@@ -111,6 +129,12 @@ class AppLocalizationsFr extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Actualisation automatique';
@override
String get common_interval => 'Intervalle';
@override
String get scanner_title => 'MeshCore Open';
@@ -803,11 +827,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batterie';
@@ -1009,6 +1028,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contacts_newGroup => 'Nouveau Groupe';
@override
String get contacts_moreOptions => 'Plus d\'options';
@override
String get contacts_searchOpen => 'Rechercher des contacts';
@override
String get contacts_searchClose => 'Recherche avancée';
@override
String get contacts_groupName => 'Nom du groupe';
@@ -1491,35 +1519,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :';
@override
String get chat_pathManagement => 'Gestion des chemins';
@override
String get chat_ShowAllPaths => 'Afficher tous les chemins';
@override
String get chat_routingMode => 'Mode de routage';
@override
String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)';
@override
String get chat_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get chat_recentAckPaths =>
'Chemins ACK récents (touchez pour utiliser) :';
@override
String get chat_pathHistoryFull =>
'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.';
@override
String get chat_hopSingular => 'saut';
@override
String get chat_hopPlural => 'sauts';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1531,12 +1530,6 @@ class AppLocalizationsFr extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Succès';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Supprimer le chemin';
@@ -1544,53 +1537,146 @@ class AppLocalizationsFr extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.';
@override
String get chat_pathActions => 'Actions du chemin :';
@override
String get chat_setCustomPath => 'Définir un chemin personnalisé';
@override
String get chat_setCustomPathSubtitle =>
'Spécifier manuellement le chemin de routage';
@override
String get chat_clearPath => 'Effacer le chemin';
@override
String get chat_clearPathSubtitle =>
'Forcer la redécouverte lors de la prochaine envoi';
@override
String get chat_pathCleared =>
'Le chemin est dégagé. Le prochain message redécouvrira le tracé.';
@override
String get chat_floodModeSubtitle =>
'Désactive l\'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d\'application pour rebasculer en mode auto par la suite.';
@override
String get chat_floodModeEnabled =>
'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.';
@override
String get chat_fullPath => 'Chemin complet';
@override
String get chat_pathDetailsNotAvailable =>
'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.';
String get routing_title => 'Planification des itinéraires';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Chemin défini : $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Voiture';
@override
String get routing_modeFlood => 'Inondation';
@override
String get routing_modeManual => 'Manuel';
@override
String get routing_modeAutoHint =>
'Sélectionne automatiquement le chemin le plus connu, et utilise la méthode de \"inondation\" si aucun chemin n\'est connu.';
@override
String get routing_modeFloodHint =>
'Diffusion via tous les répéteurs. La méthode la plus fiable, mais qui utilise plus de temps d\'antenne.';
@override
String get routing_modeManualHint =>
'Il suit toujours le chemin précis que vous avez défini.';
@override
String get routing_currentRoute => 'Itinéraire actuel';
@override
String get routing_directNoHops => 'Direct — sans relais';
@override
String get routing_noPathYet =>
'Aucune voie encore trouvée. Le message suivant est envoyé jusqu\'à ce qu\'une route soit découverte.';
@override
String get routing_floodBroadcast => 'Diffusion via tous les répéteurs';
@override
String get routing_editPath => 'Modifier le chemin';
@override
String get routing_forgetPath => 'Oubliez le chemin';
@override
String get routing_knownPaths => 'Chemins connus';
@override
String get routing_knownPathsHint => 'Créez un raccourci pour y accéder.';
@override
String get routing_inUse => 'En cours d\'utilisation';
@override
String get routing_qualityStrong => 'Première étape réussie';
@override
String get routing_qualityGood => 'Première étape réussie';
@override
String get routing_qualityFair => 'Première étape réussie';
@override
String get routing_qualityWorked => 'A livré';
@override
String get routing_qualityFlood =>
'Rapporté par des informations provenant de plusieurs sources.';
@override
String get routing_qualityUntested => 'Non testé';
@override
String routing_lastWorked(String when) {
return 'a travaillé $when';
}
@override
String get routing_neverWorked => 'jamais confirmé';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Livraison en cas de inondation';
@override
String get pathEditor_title => 'Créer un chemin';
@override
String pathEditor_hopCounter(int count) {
return '$count parmi 64 houblons';
}
@override
String get pathEditor_noHops =>
'Aucun houblon ajouté pour le moment. Cliquez sur les répétiteurs ci-dessous pour les ajouter dans l\'ordre souhaité, ou enregistrez sans houblon pour envoyer directement.';
@override
String get pathEditor_addHops =>
'Ajoutez les houblons dans l\'ordre souhaité.';
@override
String get pathEditor_searchRepeaters => 'Rechercher des répétiteurs';
@override
String get pathEditor_advancedHex => 'Avancé : chemin hexadécimal brut';
@override
String get pathEditor_hexLabel => 'Préfixes hexadécimaux';
@override
String get pathEditor_hexHelper =>
'Deux caractères hexadécimaux par saut, séparés par des virgules.';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Incorrect : $tokens';
}
@override
String get pathEditor_tooManyHops => 'Maximum 64 sauts';
@override
String get pathEditor_usePath => 'Utilisez ce chemin.';
@override
String get pathEditor_removeHop => 'Éliminer le haricot';
@override
String get pathEditor_unknownHop => 'Répéteur non identifié';
@override
String get chat_pathSavedLocally =>
'Sauvegardé localement. Connectez-vous pour synchroniser.';
@@ -2062,66 +2148,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_clear => 'Effacer';
@override
String path_currentPath(String path) {
return 'Chemin actuel : $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Utiliser $count $_temp0 chemin';
}
@override
String get path_enterCustomPath => 'Entrer un chemin personnalisé';
@override
String get path_currentPathLabel => 'Chemin actuel';
@override
String get path_hexPrefixInstructions =>
'Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.';
@override
String get path_hexPrefixExample =>
'Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).';
@override
String get path_labelHexPrefixes => 'Préfixes hexadécimaux';
@override
String get path_helperMaxHops =>
'Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)';
@override
String get path_selectFromContacts => 'Sélectionner à partir des contacts :';
@override
String get path_noRepeatersFound =>
'Aucun répéteur ou room server n\'a été trouvé.';
@override
String get path_customPathsRequire =>
'Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Préfixes hexadécimaux invalides : $prefixes';
}
@override
String get path_tooLong =>
'Le chemin est trop long. Maximum 64 sauts autorisés.';
@override
String get path_setPath => 'Définir le chemin';
@override
String get repeater_management => 'Gestion des répéteurs';
@@ -2187,16 +2220,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_routingMode => 'Mode de routage';
@override
String get repeater_autoUseSavedPath =>
'Auto (utiliser le chemin sauvegardé)';
@override
String get repeater_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get repeater_pathManagement => 'Gestion des chemins';
@override
String get repeater_refresh => 'Rafraîchir';
@@ -3305,6 +3328,139 @@ class AppLocalizationsFr extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Entrée numérique';
@override
String get telemetry_digitalOutputLabel => 'Sortie numérique';
@override
String get telemetry_analogInputLabel => 'Entrée analogique';
@override
String get telemetry_analogOutputLabel => 'Sortie analogique';
@override
String get telemetry_genericLabel => 'Capteur générique';
@override
String get telemetry_luminosityLabel => 'Luminosité';
@override
String get telemetry_presenceLabel => 'Présence';
@override
String get telemetry_humidityLabel => 'Humidité';
@override
String get telemetry_accelerometerLabel => 'Accéléromètre';
@override
String get telemetry_pressureLabel => 'Pression';
@override
String get telemetry_altitudeLabel => 'Altitude';
@override
String get telemetry_frequencyLabel => 'Fréquence';
@override
String get telemetry_percentageLabel => 'Pourcentage';
@override
String get telemetry_concentrationLabel => 'Concentration';
@override
String get telemetry_powerLabel => 'Puissance';
@override
String get telemetry_distanceLabel => 'Distance';
@override
String get telemetry_energyLabel => 'Énergie';
@override
String get telemetry_directionLabel => 'Direction';
@override
String get telemetry_timeLabel => 'Heure';
@override
String get telemetry_gyrometerLabel => 'Gyromètre';
@override
String get telemetry_colourLabel => 'Couleur';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Interrupteur';
@override
String get telemetry_polylineLabel => 'Polyligne';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Nombre de requêtes';
@override
String get telemetry_error => 'Impossible de récupérer les données';
@override
String get neighbors_receivedData => 'Données des voisins reçues';
@@ -4344,4 +4500,28 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Zoomez';
@override
String get map_zoomOut => 'Zoomez';
@override
String get map_centerMap => 'Carte du centre';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth nécessite un navigateur Chromium.';
@override
String channels_communityShortId(String id) {
return 'ID : $id';
}
@override
String get pathTrace_legendGpsConfirmed => 'Le GPS a confirmé.';
@override
String get pathTrace_legendInferred => 'Position déduite';
}
+320 -139
View File
@@ -92,6 +92,24 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get common_disable => 'Leteteszt';
@override
String get common_undo => 'Még egyszer';
@override
String get messageStatus_sent => 'Elküldve';
@override
String get messageStatus_delivered => 'Szállítva';
@override
String get messageStatus_pending => 'Elküldés';
@override
String get messageStatus_failed => 'Nem sikerült elküldeni';
@override
String get messageStatus_repeated => 'Ismételtem';
@override
String get common_reboot => 'Újraindítás';
@@ -111,6 +129,12 @@ class AppLocalizationsHu extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatikus frissítés';
@override
String get common_interval => 'Intervallum';
@override
String get scanner_title => 'MeshCore nyitott';
@@ -801,11 +825,6 @@ class AppLocalizationsHu extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Akku';
@@ -1009,6 +1028,15 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get contacts_newGroup => 'Új csoport';
@override
String get contacts_moreOptions => 'További lehetőségek';
@override
String get contacts_searchOpen => 'Keresssz kapcsolatokat';
@override
String get contacts_searchClose => 'Teljesítse a keresést';
@override
String get contacts_groupName => 'Csoport neve';
@@ -1494,36 +1522,6 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Hex-dump:';
@override
String get chat_pathManagement => 'Útvonal-kezelés';
@override
String get chat_ShowAllPaths => 'Mutasson meg minden útvonalat';
@override
String get chat_routingMode => 'Útvonal-kezelési mód';
@override
String get chat_autoUseSavedPath =>
'Automatikus (az eddigi útvonal használata)';
@override
String get chat_forceFloodMode => 'Erőforrás-alapú áramlás mód';
@override
String get chat_recentAckPaths =>
'Legutóbbi használt útvonalak (gombra kattintva):';
@override
String get chat_pathHistoryFull =>
'Az előző lépések listája teljes. Törölj ki a bejegyzéseket, hogy újokat hozzáadhatsd.';
@override
String get chat_hopSingular => 'ugor';
@override
String get chat_hopPlural => 'babér';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1535,12 +1533,6 @@ class AppLocalizationsHu extends AppLocalizations {
return '$count $_temp0';
}
@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';
@@ -1548,52 +1540,148 @@ class AppLocalizationsHu extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.';
@override
String get chat_pathActions => 'Céltúrások:';
@override
String get chat_setCustomPath => 'Beállítsd a saját útvonalat';
@override
String get chat_setCustomPathSubtitle => 'Kézzel megadott útvonal';
@override
String get chat_clearPath => 'Egyértelmű út';
@override
String get chat_clearPathSubtitle =>
'A parancs új küldéskor újra kell aktivizálnia.';
@override
String get chat_pathCleared =>
'Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.';
@override
String get chat_floodModeSubtitle =>
'Használja a \"útvonal\" kapcsolót az alkalmazás sávjában.';
@override
String get chat_floodModeEnabled =>
'Árvízvédelmi mód bekapcsolva. A visszaállítás a alkalmazásban található útvonal ikon segítségével.';
@override
String get chat_fullPath => 'Teljes elérési út';
@override
String get chat_pathDetailsNotAvailable =>
'Az útvonal részletei még nem elérhetők. Próbálja meg küldeni egy üzenetet, hogy frissítse az információkat.';
String get routing_title => 'Útvonal meghatározás';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Path set: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Autó';
@override
String get routing_modeFlood => 'Áradás';
@override
String get routing_modeManual => 'Használati útmutató';
@override
String get routing_modeAutoHint =>
'Automatikusan kiválasztja a legismertebb útvonalat, és ha egyik sem ismert, akkor \"vízzel\" tölti ki.';
@override
String get routing_modeFloodHint =>
'Átvisszaadások minden erősítőn keresztül. A legmegbízhatóbb megoldás, de több időt igényel.';
@override
String get routing_modeManualHint =>
'Mindig pontosan az útvonalat követi, amelyet megad.';
@override
String get routing_currentRoute => 'Jelenlegi útvonal';
@override
String get routing_directNoHops => 'Közvetlen nincs átjáró állomás';
@override
String get routing_noPathYet =>
'Még nincs útvonal. A következő üzenet a keresésig vár.';
@override
String get routing_floodBroadcast =>
'Azonnali továbbítás minden erősítőn keresztül.';
@override
String get routing_editPath => 'Útvonal szerkesztése';
@override
String get routing_forgetPath => 'Felejtsd el a útvonalat';
@override
String get routing_knownPaths => 'Jellegzetes útvonalak';
@override
String get routing_knownPathsHint =>
'Készíts egy útvonalat, hogy átválhass rá.';
@override
String get routing_inUse => 'Használatban';
@override
String get routing_qualityStrong => 'Erős első lépés';
@override
String get routing_qualityGood => 'Jó első lépés';
@override
String get routing_qualityFair => 'Jó első lépés';
@override
String get routing_qualityWorked => 'Előállított';
@override
String get routing_qualityFlood =>
'Információt hallottam a katasztrófa miatt.';
@override
String get routing_qualityUntested => 'Vizsgálatnak nem подвержен';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'sosem megerősítve';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Vízparti szállítás';
@override
String get pathEditor_title => 'Út megépítése';
@override
String pathEditor_hopCounter(int count) {
return '$count db 64-ből';
}
@override
String get pathEditor_noHops =>
'Még nem adtam hozzá a bazsalikomot. A lent található gombokat használhatod, hogy sorrendben adjd hozzá, vagy mentheted anélkül, hogy bazsalikomot adnál hozzá, hogy közvetlenül elküldd.';
@override
String get pathEditor_addHops =>
'Adja hozzá a bazsaidat a megfelelő sorrendben.';
@override
String get pathEditor_searchRepeaters => 'Ismétlő eszközök keresése';
@override
String get pathEditor_advancedHex => 'Haladó szint: alapvető hex-út';
@override
String get pathEditor_hexLabel => 'Hex előtagok';
@override
String get pathEditor_hexHelper =>
'Két hatjegyű szám minden lépésen, amelyek egymástól elválasztják a kommák.';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Érvénytelen: $tokens';
}
@override
String get pathEditor_tooManyHops => 'A maximális szám 64.';
@override
String get pathEditor_usePath => 'Használja ezt az útvonalat.';
@override
String get pathEditor_removeHop => 'Távolítsa el a bazsalikomot';
@override
String get pathEditor_unknownHop => 'Tudatlan erősítő';
@override
String get chat_pathSavedLocally =>
'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.';
@@ -2064,67 +2152,13 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get common_clear => 'Egyértelmű';
@override
String path_currentPath(String path) {
return 'Jelenlegi útvonal: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'ugrások',
one: 'ugrás',
);
return '$count $_temp0 útvonal használata';
}
@override
String get path_enterCustomPath => 'Adja meg a saját elérési útvonalat';
@override
String get path_currentPathLabel => 'Jelenlegi útvonal';
@override
String get path_hexPrefixInstructions =>
'Adja meg a 2 karakteres hexadecimális előtagokat minden lépéshez, tagolva kommával.';
@override
String get path_hexPrefixExample =>
'Példa: A1, F2, 3C (minden csomó az első részét használja a nyilvános kulcsából)';
@override
String get path_labelHexPrefixes => 'Út (hex-prefixek)';
@override
String get path_helperMaxHops =>
'A maximális hossz 64 karakter. Minden előző rész 2 hatos számjegyből áll (1 bájt).';
@override
String get path_selectFromContacts =>
'Válasszon a kontaktlista elembek közül:';
@override
String get path_noRepeatersFound =>
'Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.';
@override
String get path_customPathsRequire =>
'Az egyedi útvonalaknak szükségük van átjáró pontokra, amelyek képesek üzeneteket továbbítani.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Érvénytelen hexadecimális előtagok: $prefixes';
}
@override
String get path_tooLong =>
'Az út túl hosszú. A maximális engedélyezett lépések száma 64.';
@override
String get path_setPath => 'Útvonal meghatározása';
@override
String get repeater_management => 'Adatkapcsolás kezelése';
@@ -2190,16 +2224,6 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get repeater_routingMode => 'Útvonal-kezelési mód';
@override
String get repeater_autoUseSavedPath =>
'Automatikus (az eddigi útvonal használata)';
@override
String get repeater_forceFloodMode => 'Erőforrás-alapú áramlás mód';
@override
String get repeater_pathManagement => 'Útvonal-kezelés';
@override
String get repeater_refresh => 'Újrafriszol';
@@ -3298,6 +3322,139 @@ class AppLocalizationsHu extends AppLocalizations {
return '$celsius °C / $fahrenheit °F';
}
@override
String get telemetry_digitalInputLabel => 'Digitális bemenet';
@override
String get telemetry_digitalOutputLabel => 'Digitális kimenet';
@override
String get telemetry_analogInputLabel => 'Analóg bemenet';
@override
String get telemetry_analogOutputLabel => 'Analóg kimenet';
@override
String get telemetry_genericLabel => 'Általános érzékelő';
@override
String get telemetry_luminosityLabel => 'Fényerő';
@override
String get telemetry_presenceLabel => 'Jelenlét';
@override
String get telemetry_humidityLabel => 'Páratartalom';
@override
String get telemetry_accelerometerLabel => 'Gyorsulásmérő';
@override
String get telemetry_pressureLabel => 'Nyomás';
@override
String get telemetry_altitudeLabel => 'Magasság';
@override
String get telemetry_frequencyLabel => 'Frekvencia';
@override
String get telemetry_percentageLabel => 'Százalék';
@override
String get telemetry_concentrationLabel => 'Koncentráció';
@override
String get telemetry_powerLabel => 'Teljesítmény';
@override
String get telemetry_distanceLabel => 'Távolság';
@override
String get telemetry_energyLabel => 'Energia';
@override
String get telemetry_directionLabel => 'Irány';
@override
String get telemetry_timeLabel => 'Idő';
@override
String get telemetry_gyrometerLabel => 'Giroszkóp';
@override
String get telemetry_colourLabel => 'Szín';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Kapcsoló';
@override
String get telemetry_polylineLabel => 'Töröttvonal';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Kérések száma';
@override
String get telemetry_error => 'Nem sikerült lekérni az adatokat';
@override
String get neighbors_receivedData => 'Kapott szomszédok adatait';
@@ -4331,4 +4488,28 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Nagyítva';
@override
String get map_zoomOut => 'Kicsökkentett nézet';
@override
String get map_centerMap => 'Központi tér térkép';
@override
String get chrome_bluetoothRequiresChromium =>
'A Web Bluetooth-hoz egy Chromium-alapú böngésző szükséges.';
@override
String channels_communityShortId(String id) {
return 'Az azonosító: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS-en megerősítve';
@override
String get pathTrace_legendInferred => 'Feltehető helyzet';
}
+320 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_disable => 'Disattivare';
@override
String get common_undo => 'Annulla';
@override
String get messageStatus_sent => 'Invia';
@override
String get messageStatus_delivered => 'Consegnato';
@override
String get messageStatus_pending => 'Invio';
@override
String get messageStatus_failed => 'Impossibile inviare';
@override
String get messageStatus_repeated => 'Sentito ripetutamente';
@override
String get common_reboot => 'Riavvia';
@@ -111,6 +129,12 @@ class AppLocalizationsIt extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Aggiornamento automatico';
@override
String get common_interval => 'Intervallo';
@override
String get scanner_title => 'MeshCore Open';
@@ -800,11 +824,6 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batteria';
@@ -1005,6 +1024,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_newGroup => 'Nuovo Gruppo';
@override
String get contacts_moreOptions => 'Ulteriori opzioni';
@override
String get contacts_searchOpen => 'Cerca contatti';
@override
String get contacts_searchClose => 'Ricerca avanzata';
@override
String get contacts_groupName => 'Nome gruppo';
@@ -1487,34 +1515,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Dumpa Esadecimale:';
@override
String get chat_pathManagement => 'Gestione Percorsi';
@override
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
@override
String get chat_routingMode => 'Modalità di routing';
@override
String get chat_autoUseSavedPath => 'Utilizza il percorso salvato';
@override
String get chat_forceFloodMode => 'Modalità Inondamento Forzato';
@override
String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):';
@override
String get chat_pathHistoryFull =>
'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.';
@override
String get chat_hopSingular => 'salta';
@override
String get chat_hopPlural => 'salta';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1526,12 +1526,6 @@ class AppLocalizationsIt extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'successi';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Rimuovi percorso';
@@ -1539,53 +1533,148 @@ class AppLocalizationsIt extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.';
@override
String get chat_pathActions => 'Azioni Percorso:';
@override
String get chat_setCustomPath => 'Imposta Percorso Personalizzato';
@override
String get chat_setCustomPathSubtitle =>
'Specifica manualmente il percorso di routing';
@override
String get chat_clearPath => 'Cancella Percorso';
@override
String get chat_clearPathSubtitle =>
'Riprova la scoperta alla prossima invio';
@override
String get chat_pathCleared =>
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.';
@override
String get chat_floodModeSubtitle =>
'Utilizza l\'interruttore di routing nella barra delle applicazioni';
@override
String get chat_floodModeEnabled =>
'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.';
@override
String get chat_fullPath => 'Percorso Completo';
@override
String get chat_pathDetailsNotAvailable =>
'I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.';
String get routing_title => 'Instradamento';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Percorso impostato: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Auto';
@override
String get routing_modeFlood => 'Inondazione';
@override
String get routing_modeManual => 'Manuale';
@override
String get routing_modeAutoHint =>
'Seleziona automaticamente il percorso più noto, e in caso di assenza di informazioni, utilizza un percorso casuale.';
@override
String get routing_modeFloodHint =>
'Trasmissioni tramite ogni ripetitore. Il metodo più affidabile, ma richiede più tempo di trasmissione.';
@override
String get routing_modeManualHint =>
'Invia sempre esattamente il percorso che hai definito.';
@override
String get routing_currentRoute => 'Percorso attuale';
@override
String get routing_directNoHops =>
'Diretto — senza passaggi tramite ripetitori';
@override
String get routing_noPathYet =>
'Al momento non è stata individuata alcuna via. Il messaggio viene inviato ripetutamente finché non viene trovata una rotta.';
@override
String get routing_floodBroadcast =>
'Trasmissione attraverso ogni ripetitore';
@override
String get routing_editPath => 'Percorso di modifica';
@override
String get routing_forgetPath => 'Dimentica il percorso';
@override
String get routing_knownPaths => 'Percorsi noti';
@override
String get routing_knownPathsHint =>
'Seleziona un percorso per accedere a questa opzione.';
@override
String get routing_inUse => 'In uso';
@override
String get routing_qualityStrong => 'Primo salto molto deciso';
@override
String get routing_qualityGood => 'Primo tentativo di successo';
@override
String get routing_qualityFair => 'Primo salto di qualità';
@override
String get routing_qualityWorked => 'È stato consegnato';
@override
String get routing_qualityFlood => 'Ho sentito tramite un messaggio urgente';
@override
String get routing_qualityUntested => 'Non testato';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'mai confermato';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Consegna in caso di alluvione';
@override
String get pathEditor_title => 'Creare percorso';
@override
String pathEditor_hopCounter(int count) {
return '$count tra 64 varietà di luppolo';
}
@override
String get pathEditor_noHops =>
'Al momento non ci sono ingredienti aggiuntivi. Per aggiungerli nell\'ordine desiderato, cliccate sui ripetitori sottostanti. In alternativa, potete salvare la ricetta senza ingredienti aggiuntivi per inviarla direttamente.';
@override
String get pathEditor_addHops =>
'Aggiungere i luppoli nell\'ordine desiderato.';
@override
String get pathEditor_searchRepeaters => 'Ricerca ripetitori';
@override
String get pathEditor_advancedHex => 'Avanzato: percorso esadecimale grezzo';
@override
String get pathEditor_hexLabel => 'Prefissi esadecimali';
@override
String get pathEditor_hexHelper =>
'Due caratteri esadecimali per ogni salto, separati da virgole.';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Non valido: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Massimo 64 orari';
@override
String get pathEditor_usePath => 'Utilizza questo percorso';
@override
String get pathEditor_removeHop => 'Rimuovere il luppolo';
@override
String get pathEditor_unknownHop => 'Ripetitore sconosciuto';
@override
String get chat_pathSavedLocally =>
'Salvatato localmente. Connetti per sincronizzare.';
@@ -2053,66 +2142,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_clear => 'Cancella';
@override
String path_currentPath(String path) {
return 'Percorso corrente: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Utilizzare $count $_temp0 percorso';
}
@override
String get path_enterCustomPath => 'Inserisci percorso personalizzato';
@override
String get path_currentPathLabel => 'Percorso corrente';
@override
String get path_hexPrefixInstructions =>
'Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.';
@override
String get path_hexPrefixExample =>
'Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)';
@override
String get path_labelHexPrefixes => 'Prefisso esadecimale (percorso)';
@override
String get path_helperMaxHops =>
'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)';
@override
String get path_selectFromContacts => 'Seleziona da contatti:';
@override
String get path_noRepeatersFound =>
'Non sono stati trovati ripetitori o server di stanza.';
@override
String get path_customPathsRequire =>
'I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Prefissi esadecimali non validi: $prefixes';
}
@override
String get path_tooLong =>
'Il percorso è troppo lungo. Massimo 64 salti consentiti.';
@override
String get path_setPath => 'Imposta Percorso';
@override
String get repeater_management => 'Gestione Ripetitori';
@@ -2179,15 +2215,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_routingMode => 'Modalità di routing';
@override
String get repeater_autoUseSavedPath => 'Percorso salvato automatico';
@override
String get repeater_forceFloodMode => 'Modalità Inondamento Forzato';
@override
String get repeater_pathManagement => 'Gestione dei percorsi';
@override
String get repeater_refresh => 'Aggiorna';
@@ -3288,6 +3315,139 @@ class AppLocalizationsIt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Ingresso digitale';
@override
String get telemetry_digitalOutputLabel => 'Uscita digitale';
@override
String get telemetry_analogInputLabel => 'Ingresso analogico';
@override
String get telemetry_analogOutputLabel => 'Uscita analogica';
@override
String get telemetry_genericLabel => 'Sensore generico';
@override
String get telemetry_luminosityLabel => 'Luminosità';
@override
String get telemetry_presenceLabel => 'Presenza';
@override
String get telemetry_humidityLabel => 'Umidità';
@override
String get telemetry_accelerometerLabel => 'Accelerometro';
@override
String get telemetry_pressureLabel => 'Pressione';
@override
String get telemetry_altitudeLabel => 'Altitudine';
@override
String get telemetry_frequencyLabel => 'Frequenza';
@override
String get telemetry_percentageLabel => 'Percentuale';
@override
String get telemetry_concentrationLabel => 'Concentrazione';
@override
String get telemetry_powerLabel => 'Potenza';
@override
String get telemetry_distanceLabel => 'Distanza';
@override
String get telemetry_energyLabel => 'Energia';
@override
String get telemetry_directionLabel => 'Direzione';
@override
String get telemetry_timeLabel => 'Ora';
@override
String get telemetry_gyrometerLabel => 'Giroscopio';
@override
String get telemetry_colourLabel => 'Colore';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Interruttore';
@override
String get telemetry_polylineLabel => 'Polilinea';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Numero di richieste';
@override
String get telemetry_error => 'Impossibile recuperare i dati';
@override
String get neighbors_receivedData => 'Ricevute dati vicini';
@@ -4320,4 +4480,28 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Ingrandisci';
@override
String get map_zoomOut => 'Riduci la visualizzazione';
@override
String get map_centerMap => 'Mappa del centro';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth richiede un browser basato su Chromium.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'Il GPS conferma';
@override
String get pathTrace_legendInferred => 'Posizione dedotta';
}
File diff suppressed because it is too large Load Diff
+312 -132
View File
@@ -92,6 +92,24 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get common_disable => '비활성화';
@override
String get common_undo => '취소';
@override
String get messageStatus_sent => '발송';
@override
String get messageStatus_delivered => '배송 완료';
@override
String get messageStatus_pending => '발송';
@override
String get messageStatus_failed => '발송 실패';
@override
String get messageStatus_repeated => '반복적으로 들었습니다';
@override
String get common_reboot => '재부팅';
@@ -111,6 +129,12 @@ class AppLocalizationsKo extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => '자동 새로고침';
@override
String get common_interval => '간격';
@override
String get scanner_title => 'MeshCore 공개';
@@ -755,11 +779,6 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get appSettings_maxMessageRetriesSubtitle => '메시지를 실패로 처리하기 전 시도 횟수';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => '배터리';
@@ -951,6 +970,15 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get contacts_newGroup => '새로운 그룹';
@override
String get contacts_moreOptions => '더 많은 옵션';
@override
String get contacts_searchOpen => '연락처 검색';
@override
String get contacts_searchClose => '검색 닫기';
@override
String get contacts_groupName => '그룹 이름';
@@ -1417,34 +1445,6 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get debugFrame_hexDump => '헥스 덤프:';
@override
String get chat_pathManagement => '경로 관리';
@override
String get chat_ShowAllPaths => '모든 경로 표시';
@override
String get chat_routingMode => '라우팅 방식';
@override
String get chat_autoUseSavedPath => '자동 (저장된 경로 사용)';
@override
String get chat_forceFloodMode => '강수 모드 활성화';
@override
String get chat_recentAckPaths => '최근 사용한 ACK 경로 (사용하려면 탭):';
@override
String get chat_pathHistoryFull =>
'이력 기록은 이미 가득 차 있습니다. 항목을 삭제하여 새로운 항목을 추가할 수 있습니다.';
@override
String get chat_hopSingular => '점프';
@override
String get chat_hopPlural => '';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1456,61 +1456,146 @@ class AppLocalizationsKo extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => '성공 사례';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => '경로 제거';
@override
String get chat_noPathHistoryYet => '아직 경로 기록이 없습니다.\n경로를 찾기 위해 메시지를 보내세요.';
@override
String get chat_pathActions => '경로 작업:';
@override
String get chat_setCustomPath => '사용자 지정 경로 설정';
@override
String get chat_setCustomPathSubtitle => '수동으로 경로를 지정';
@override
String get chat_clearPath => '명확한 길';
@override
String get chat_clearPathSubtitle => '다음 전송 시, 강제 재전송 설정';
@override
String get chat_pathCleared => '경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.';
@override
String get chat_floodModeSubtitle => '앱 바에서 라우팅 스위치를 사용';
@override
String get chat_floodModeEnabled =>
'홍수 모드 활성화됨. 앱 바의 경로 아이콘을 사용하여 다시 전환할 수 있습니다.';
@override
String get chat_fullPath => '전체 경로';
@override
String get chat_pathDetailsNotAvailable =>
'경로 정보는 아직 제공되지 않습니다. 메시지를 보내어 다시 시도해 보세요.';
String get routing_title => '라우팅';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Path set: $hopCount $_temp0 - $status';
String get routing_modeAuto => '자동';
@override
String get routing_modeFlood => '홍수';
@override
String get routing_modeManual => '사용 설명서';
@override
String get routing_modeAutoHint =>
'가장 잘 알려진 경로를 자동으로 선택하고, 경로가 없을 경우에는 무작위로 경로를 선택합니다.';
@override
String get routing_modeFloodHint =>
'모든 증폭기를 통해 방송됩니다. 가장 안정적이지만, 더 많은 송출 시간을 사용합니다.';
@override
String get routing_modeManualHint => '항상 설정하신 정확한 경로를 따라 이동합니다.';
@override
String get routing_currentRoute => '현재 경로';
@override
String get routing_directNoHops => '직접 연결 중계 장치 사용 없이';
@override
String get routing_noPathYet => '아직 경로가 없습니다. 다음 메시지가 도착할 때까지 계속 탐색합니다.';
@override
String get routing_floodBroadcast => '모든 증폭기를 통해 방송';
@override
String get routing_editPath => '경로 편집';
@override
String get routing_forgetPath => '길을 잊어라';
@override
String get routing_knownPaths => '알려진 경로';
@override
String get routing_knownPathsHint => '해당 항목으로 전환하기 위한 경로를 선택합니다.';
@override
String get routing_inUse => '사용 중';
@override
String get routing_qualityStrong => '강력한 첫 번째 단계';
@override
String get routing_qualityGood => '좋은 첫 시작';
@override
String get routing_qualityFair => '처음 시도';
@override
String get routing_qualityWorked => '완료됨';
@override
String get routing_qualityFlood => '홍수 피해 상황을 통해 들었습니다.';
@override
String get routing_qualityUntested => '검증되지 않음';
@override
String routing_lastWorked(String when) {
return '$when에 일했습니다';
}
@override
String get routing_neverWorked => '확인되지 않음';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => '홍수 피해 지역 배송';
@override
String get pathEditor_title => '경로 만들기';
@override
String pathEditor_hopCounter(int count) {
return '64개의 홉 중 $count';
}
@override
String get pathEditor_noHops =>
'현재 홉은 추가되지 않았습니다. 아래의 탭을 사용하여 순서대로 추가하거나, 홉 없이 바로 전송하려면 \"홉 없음\"으로 저장하십시오.';
@override
String get pathEditor_addHops => '홉을 순서대로 첨가해주세요.';
@override
String get pathEditor_searchRepeaters => '반복 검색';
@override
String get pathEditor_advancedHex => '고급: 원시 헥스 경로';
@override
String get pathEditor_hexLabel => '헥스 접두사';
@override
String get pathEditor_hexHelper => '각 홉마다 2개의 6자리 숫자, 쉼표로 구분';
@override
String pathEditor_invalidTokens(String tokens) {
return '유효하지 않음: $tokens';
}
@override
String get pathEditor_tooManyHops => '최대 64개의 홉';
@override
String get pathEditor_usePath => '이 경로를 사용하세요';
@override
String get pathEditor_removeHop => '홉 제거';
@override
String get pathEditor_unknownHop => '알 수 없는 중계기';
@override
String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결';
@@ -1964,64 +2049,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get common_clear => '명확하게';
@override
String path_currentPath(String path) {
return '현재 경로: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Using $count $_temp0 path';
}
@override
String get path_enterCustomPath => '사용자 지정 경로 입력';
@override
String get path_currentPathLabel => '현재 경로';
@override
String get path_hexPrefixInstructions =>
'각 단계에 대한 2자리 헥사데진 접두사를 쉼표로 구분하여 입력하세요.';
@override
String get path_hexPrefixExample =>
'예시: A1, F2, 3C (각 노드는 자신의 공개 키의 첫 번째 바이트를 사용)';
@override
String get path_labelHexPrefixes => '경로 (헥스 접두사)';
@override
String get path_helperMaxHops =>
'최대 64개의 홉. 각 접두사는 2개의 16진수 문자(1바이트)로 구성됩니다.';
@override
String get path_selectFromContacts => '또 연락처 목록에서 선택:';
@override
String get path_noRepeatersFound => '반복 장치 또는 서버는 찾을 수 없습니다.';
@override
String get path_customPathsRequire =>
'사용자 정의 경로에는 메시지를 전달할 수 있는 중간 경로가 필요합니다.';
@override
String path_invalidHexPrefixes(String prefixes) {
return '유효하지 않은 16진수 접두사: $prefixes';
}
@override
String get path_tooLong => '경로가 너무 길어. 최대 64개의 연결만 허용됩니다.';
@override
String get path_setPath => '경로 설정';
@override
String get repeater_management => '리피터 관리';
@@ -2083,15 +2116,6 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get repeater_routingMode => '라우팅 방식';
@override
String get repeater_autoUseSavedPath => '자동 (저장된 경로 사용)';
@override
String get repeater_forceFloodMode => '강수 모드 활성화';
@override
String get repeater_pathManagement => '경로 관리';
@override
String get repeater_refresh => '새롭게';
@@ -3108,6 +3132,139 @@ class AppLocalizationsKo extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => '디지털 입력';
@override
String get telemetry_digitalOutputLabel => '디지털 출력';
@override
String get telemetry_analogInputLabel => '아날로그 입력';
@override
String get telemetry_analogOutputLabel => '아날로그 출력';
@override
String get telemetry_genericLabel => '일반 센서';
@override
String get telemetry_luminosityLabel => '조도';
@override
String get telemetry_presenceLabel => '존재 감지';
@override
String get telemetry_humidityLabel => '습도';
@override
String get telemetry_accelerometerLabel => '가속도계';
@override
String get telemetry_pressureLabel => '압력';
@override
String get telemetry_altitudeLabel => '고도';
@override
String get telemetry_frequencyLabel => '주파수';
@override
String get telemetry_percentageLabel => '백분율';
@override
String get telemetry_concentrationLabel => '농도';
@override
String get telemetry_powerLabel => '전력';
@override
String get telemetry_distanceLabel => '거리';
@override
String get telemetry_energyLabel => '에너지';
@override
String get telemetry_directionLabel => '방향';
@override
String get telemetry_timeLabel => '시간';
@override
String get telemetry_gyrometerLabel => '자이로미터';
@override
String get telemetry_colourLabel => '색상';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => '스위치';
@override
String get telemetry_polylineLabel => '폴리라인';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => '요청 수';
@override
String get telemetry_error => '데이터를 가져올 수 없습니다';
@override
String get neighbors_receivedData => '이웃 정보 수집';
@@ -4086,4 +4243,27 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => '줌 인';
@override
String get map_zoomOut => '줌 아웃';
@override
String get map_centerMap => '중심 지도';
@override
String get chrome_bluetoothRequiresChromium => '웹 블루투스는 크롬 브라우저가 필요합니다.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS 확인 완료';
@override
String get pathTrace_legendInferred => '추론된 위치';
}
+316 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_disable => 'Uitschakelen';
@override
String get common_undo => 'Achterhalen/Annuleren';
@override
String get messageStatus_sent => 'Verzonden';
@override
String get messageStatus_delivered => 'Leverd';
@override
String get messageStatus_pending => 'Verzenden';
@override
String get messageStatus_failed => 'Niet verzonden';
@override
String get messageStatus_repeated => 'Hearsay, herhaald';
@override
String get common_reboot => 'Herstarten';
@@ -111,6 +129,12 @@ class AppLocalizationsNl extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatisch vernieuwen';
@override
String get common_interval => 'Tijdsinterval';
@override
String get scanner_title => 'MeshCore Open';
@@ -792,11 +816,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batterij';
@@ -997,6 +1016,15 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contacts_newGroup => 'Nieuwe Groep';
@override
String get contacts_moreOptions => 'Meer opties';
@override
String get contacts_searchOpen => 'Zoek contactpersonen';
@override
String get contacts_searchClose => 'Zoeken';
@override
String get contacts_groupName => 'Groepnaam';
@@ -1474,34 +1502,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Hex-dump:';
@override
String get chat_pathManagement => 'Beheer van Paden';
@override
String get chat_ShowAllPaths => 'Toon alle paden';
@override
String get chat_routingMode => 'Routeerwijze';
@override
String get chat_autoUseSavedPath => 'Automatisch (gebruik opgeslagen pad)';
@override
String get chat_forceFloodMode => 'Dwing Floodsmodus';
@override
String get chat_recentAckPaths => 'Recente ACK Paden (tik om te gebruiken):';
@override
String get chat_pathHistoryFull =>
'De voorgeschiedenis is vol. Verwijder vermeldingen om er nieuwe aan toe te voegen.';
@override
String get chat_hopSingular => 'Hop';
@override
String get chat_hopPlural => 'hoppen';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1513,12 +1513,6 @@ class AppLocalizationsNl extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Succesvol';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Pad verwijderen';
@@ -1526,52 +1520,144 @@ class AppLocalizationsNl extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Geen geschiedenis van paden nog beschikbaar.\nVerzend een bericht om paden te ontdekken.';
@override
String get chat_pathActions => 'Padacties:';
@override
String get chat_setCustomPath => 'Stel aangepaste pad in';
@override
String get chat_setCustomPathSubtitle => 'Handmatig routepad specificeren';
@override
String get chat_clearPath => 'Duidelijke Pad';
@override
String get chat_clearPathSubtitle =>
'Dwing herontdekking bij volgende verzending';
@override
String get chat_pathCleared =>
'Pad is vrijgegeven. Volgende bericht herontdekt route.';
@override
String get chat_floodModeSubtitle =>
'Gebruik de route-schakelaar in de app-balk';
@override
String get chat_floodModeEnabled =>
'Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.';
@override
String get chat_fullPath => 'Volledige Pad';
@override
String get chat_pathDetailsNotAvailable =>
'De paddetails zijn nog niet beschikbaar. Probeer een bericht te sturen om te vernieuwen.';
String get routing_title => 'Routeplanning';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Pad ingesteld: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Auto';
@override
String get routing_modeFlood => 'Overstroming';
@override
String get routing_modeManual => 'Handleiding';
@override
String get routing_modeAutoHint =>
'Selecteert automatisch het bekendste pad, en gebruikt een flood-algoritme als er geen bekend pad is.';
@override
String get routing_modeFloodHint =>
'Uitzendingen via elke zender. De meest betrouwbare methode, maar vereist meer uitzendtijd.';
@override
String get routing_modeManualHint =>
'Stuurt altijd de exacte route die u heeft aangegeven.';
@override
String get routing_currentRoute => 'Huidige route';
@override
String get routing_directNoHops => 'Direct zonder tussenliggende schakels';
@override
String get routing_noPathYet =>
'Er is nog geen route gevonden. De berichten blijven binnenkomen totdat een route is ontdekt.';
@override
String get routing_floodBroadcast => 'Uitgestoten via elke zender.';
@override
String get routing_editPath => 'Pad bewerken';
@override
String get routing_forgetPath => 'Vergeet het pad';
@override
String get routing_knownPaths => 'Bekende routes';
@override
String get routing_knownPathsHint => 'Maak een route om er naartoe te gaan.';
@override
String get routing_inUse => 'In gebruik';
@override
String get routing_qualityStrong => 'Sterke eerste sprong';
@override
String get routing_qualityGood => 'Een goede eerste stap';
@override
String get routing_qualityFair => 'Een goede eerste hop';
@override
String get routing_qualityWorked => 'Is geleverd';
@override
String get routing_qualityFlood => 'Hears via een overstroming';
@override
String get routing_qualityUntested => 'Niet getest';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'nooit bevestigd';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes zijn behaald, $failures zijn mislukt';
}
@override
String get routing_floodDelivery => 'Levering bij overstroming';
@override
String get pathEditor_title => 'Pad creëren';
@override
String pathEditor_hopCounter(int count) {
return '$count van 64 hopgranen';
}
@override
String get pathEditor_noHops =>
'Er zijn nog geen hop toegevoegd. Klik op de onderstaande knoppen om ze in de juiste volgorde toe te voegen, of sla de bestelling op zonder hop om deze direct te versturen.';
@override
String get pathEditor_addHops => 'Voeg hop toe in de juiste volgorde.';
@override
String get pathEditor_searchRepeaters => 'Zoek naar herhaaldelijke zenders';
@override
String get pathEditor_advancedHex => 'Geavanceerd: ruwe hex-pad';
@override
String get pathEditor_hexLabel => 'Hex-voorkanten';
@override
String get pathEditor_hexHelper =>
'Twee hex-tekens per stap, gescheiden door komma\'s';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Ongeldig: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Maximaal 64 hopken';
@override
String get pathEditor_usePath => 'Gebruik deze route.';
@override
String get pathEditor_removeHop => 'Verwijder de hop';
@override
String get pathEditor_unknownHop => 'Onbekend type zender';
@override
String get chat_pathSavedLocally =>
'Opgeslagen lokaal. Verbinden om te synchroniseren.';
@@ -2040,65 +2126,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_clear => 'Schoonmaken';
@override
String path_currentPath(String path) {
return 'Huidige pad: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Gebruik $count $_temp0 pad';
}
@override
String get path_enterCustomPath => 'Voer aangepaste pad in';
@override
String get path_currentPathLabel => 'Huidige pad';
@override
String get path_hexPrefixInstructions =>
'Voer 2-letter hex-voorgiffen voor elke hop in, gescheiden door komma\'s.';
@override
String get path_hexPrefixExample =>
'Voorbeeld: A1,F2,3C (elke node gebruikt het eerste byte van zijn openbare sleutel)';
@override
String get path_labelHexPrefixes => 'Pad (hex-voorkeursletters)';
@override
String get path_helperMaxHops =>
'Maximaal 64 sprongen. Elke prefix is 2 hexadecimale tekens (1 byte)';
@override
String get path_selectFromContacts => 'Of select contacten:';
@override
String get path_noRepeatersFound => 'Geen repeaters of roomservers gevonden.';
@override
String get path_customPathsRequire =>
'Aangepaste paden vereisen tussentse overstappen die berichten kunnen doorgeven.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Ongeldige hex-voorkeursletters: $prefixes';
}
@override
String get path_tooLong =>
'Pad is te lang. Maximaal 64 sprongen zijn toegestaan.';
@override
String get path_setPath => 'Stel Pad in';
@override
String get repeater_management => 'Beheer Repeaters';
@@ -2163,16 +2196,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_routingMode => 'Routeerwijze';
@override
String get repeater_autoUseSavedPath =>
'Automatisch (gebruik opgeslagen pad)';
@override
String get repeater_forceFloodMode => 'Dwing Floodmodus Af';
@override
String get repeater_pathManagement => 'Beheer van paden';
@override
String get repeater_refresh => 'Vernieuwen';
@@ -3268,6 +3291,139 @@ class AppLocalizationsNl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Digitale ingang';
@override
String get telemetry_digitalOutputLabel => 'Digitale uitgang';
@override
String get telemetry_analogInputLabel => 'Analoge ingang';
@override
String get telemetry_analogOutputLabel => 'Analoge uitgang';
@override
String get telemetry_genericLabel => 'Algemene sensor';
@override
String get telemetry_luminosityLabel => 'Lichtsterkte';
@override
String get telemetry_presenceLabel => 'Aanwezigheid';
@override
String get telemetry_humidityLabel => 'Luchtvochtigheid';
@override
String get telemetry_accelerometerLabel => 'Versnellingsmeter';
@override
String get telemetry_pressureLabel => 'Druk';
@override
String get telemetry_altitudeLabel => 'Hoogte';
@override
String get telemetry_frequencyLabel => 'Frequentie';
@override
String get telemetry_percentageLabel => 'Percentage';
@override
String get telemetry_concentrationLabel => 'Concentratie';
@override
String get telemetry_powerLabel => 'Vermogen';
@override
String get telemetry_distanceLabel => 'Afstand';
@override
String get telemetry_energyLabel => 'Energie';
@override
String get telemetry_directionLabel => 'Richting';
@override
String get telemetry_timeLabel => 'Tijd';
@override
String get telemetry_gyrometerLabel => 'Gyrometer';
@override
String get telemetry_colourLabel => 'Kleur';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Schakelaar';
@override
String get telemetry_polylineLabel => 'Polylijn';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Aantal aanvragen';
@override
String get telemetry_error => 'Kan gegevens niet ophalen';
@override
String get neighbors_receivedData => 'Ontvangen Buurdata';
@@ -4295,4 +4451,28 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Inzoomen';
@override
String get map_zoomOut => 'Inzoomen';
@override
String get map_centerMap => 'Centraal overzicht';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth vereist een Chromium-browser.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS-locatie bevestigd';
@override
String get pathTrace_legendInferred => 'Afgeleide positie';
}
+320 -139
View File
@@ -92,6 +92,24 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_disable => 'Wyłącz';
@override
String get common_undo => 'Wycofaj';
@override
String get messageStatus_sent => 'Wysłane';
@override
String get messageStatus_delivered => 'Dostarczone';
@override
String get messageStatus_pending => 'Wysyłanie';
@override
String get messageStatus_failed => 'Nie udało się wysłać';
@override
String get messageStatus_repeated => 'Usłyszałem to wielokrotnie';
@override
String get common_reboot => 'Uruchom ponownie';
@@ -111,6 +129,12 @@ class AppLocalizationsPl extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatyczne odświeżanie';
@override
String get common_interval => 'Interwał';
@override
String get scanner_title => 'MeshCore wersja open source';
@@ -802,11 +826,6 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Bateria';
@@ -1015,6 +1034,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_newGroup => 'Nowa Grupa';
@override
String get contacts_moreOptions => 'Więcej opcji';
@override
String get contacts_searchOpen => 'Wyszukaj kontakty';
@override
String get contacts_searchClose => 'Zaawansowane wyszukiwanie';
@override
String get contacts_groupName => 'Nazwa grupy';
@@ -1497,35 +1525,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Zrzut hex:';
@override
String get chat_pathManagement => 'Zarządzanie ścieżkami';
@override
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
@override
String get chat_routingMode => 'Tryb routingu';
@override
String get chat_autoUseSavedPath => 'Automatyczne (użyj zapisanej ścieżki)';
@override
String get chat_forceFloodMode => 'Wymuś tryb zalewowy';
@override
String get chat_recentAckPaths =>
'Ostatnie ścieżki ACK (naciśnij, aby użyć):';
@override
String get chat_pathHistoryFull =>
'Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.';
@override
String get chat_hopSingular => 'skok';
@override
String get chat_hopPlural => 'skoki';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1539,12 +1538,6 @@ class AppLocalizationsPl extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Sukcesy';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Usuń ścieżkę';
@@ -1552,52 +1545,148 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Brak historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.';
@override
String get chat_pathActions => 'Działania ścieżki:';
@override
String get chat_setCustomPath => 'Ustaw ścieżkę niestandardową';
@override
String get chat_setCustomPathSubtitle => 'Ręcznie określ trasę.';
@override
String get chat_clearPath => 'Wyczyść Ścieżkę';
@override
String get chat_clearPathSubtitle =>
'Wymuś ponowne wyznaczenie trasy przy następnym wysłaniu';
@override
String get chat_pathCleared =>
'Ścieżka wyczyszczona. Następna wiadomość odnajdzie trasę.';
@override
String get chat_floodModeSubtitle =>
'Użyj przełącznika routingu w pasku narzędzi.';
@override
String get chat_floodModeEnabled =>
'Tryb zalewowy włączony. Przełącz z powrotem ikoną routingu w pasku aplikacji.';
@override
String get chat_fullPath => 'Pełna ścieżka';
@override
String get chat_pathDetailsNotAvailable =>
'Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.';
String get routing_title => 'Planowanie tras';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Ścieżka ustawiona: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Samochód';
@override
String get routing_modeFlood => 'Powódź';
@override
String get routing_modeManual => 'Instrukcja obsługi';
@override
String get routing_modeAutoHint =>
'Automatycznie wybiera najpopularniejszą ścieżkę, a w przypadku braku znanej, przechodzi do trybu \"przepływu\".';
@override
String get routing_modeFloodHint =>
'Transmisje za pośrednictwem każdego repeatera. Najbardziej niezawodna metoda, ale zużywa więcej czasu transmisji.';
@override
String get routing_modeManualHint =>
'Zawsze prowadzi dokładnie po trasie, którą określiłeś.';
@override
String get routing_currentRoute => 'Obecna trasa';
@override
String get routing_directNoHops =>
'Bezpośrednio bez pośrednictwa repeaterów';
@override
String get routing_noPathYet =>
'Na razie nie ma żadnej ścieżki. Komunikacja trwa do momentu, gdy zostanie odkryta trasa.';
@override
String get routing_floodBroadcast =>
'Transmisja za pośrednictwem każdego urządzenia powielającego';
@override
String get routing_editPath => 'Edytuj ścieżkę';
@override
String get routing_forgetPath => 'Zapomnij o ścieżce';
@override
String get routing_knownPaths => 'Znane trasy';
@override
String get routing_knownPathsHint =>
'Wybierz ścieżkę, aby przełączyć się na nią.';
@override
String get routing_inUse => 'W użyciu';
@override
String get routing_qualityStrong => 'Silny pierwszy skok';
@override
String get routing_qualityGood => 'Świetny początek';
@override
String get routing_qualityFair => 'Świetny pierwszy krzak';
@override
String get routing_qualityWorked => 'Zostało dostarczone';
@override
String get routing_qualityFlood => 'Usłyszano dzięki doniesieniom';
@override
String get routing_qualityUntested => 'Nieużywany';
@override
String routing_lastWorked(String when) {
return 'pracował $when';
}
@override
String get routing_neverWorked => 'nigdy nie zostało potwierdzone';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Dostawa w przypadku powodzi';
@override
String get pathEditor_title => 'Stworzenie ścieżki';
@override
String pathEditor_hopCounter(int count) {
return '$count z 64 rodzajów chmielu';
}
@override
String get pathEditor_noHops =>
'Na razie nie dodano żadnych chmielu. Aby dodać je w odpowiedniej kolejności, kliknij w odpowiednie przyciski poniżej, lub zapisz przepis bez chmielu, aby wysłać go bezpośrednio.';
@override
String get pathEditor_addHops => 'Dodawaj chmiel zgodnie z kolejnością.';
@override
String get pathEditor_searchRepeaters => 'Funkcje powtarzania';
@override
String get pathEditor_advancedHex =>
'Zaawansowane: ścieżka w formacie szesnastkowym';
@override
String get pathEditor_hexLabel => 'Prefiksy heksadecymalne';
@override
String get pathEditor_hexHelper =>
'Dwa znaki szesnastkowe na każdym kroku, oddzielone przecinkami';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Nieprawidłowe: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Maksymalnie 64 hopów';
@override
String get pathEditor_usePath => 'Użyj tej ścieżki.';
@override
String get pathEditor_removeHop => 'Usuń dziką psiankę';
@override
String get pathEditor_unknownHop => 'Nieznany repeater';
@override
String get chat_pathSavedLocally =>
'Zapisano lokalnie. Połącz się, aby zsynchronizować.';
@@ -2067,68 +2156,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_clear => 'Wyczyść';
@override
String path_currentPath(String path) {
return 'Aktualna ścieżka: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'skoków',
many: 'skoków',
few: 'skoki',
one: 'skok',
);
return 'Użyj ścieżki $count $_temp0.';
}
@override
String get path_enterCustomPath => 'Wprowadź własną ścieżkę';
@override
String get path_currentPathLabel => 'Aktualna ścieżka';
@override
String get path_hexPrefixInstructions =>
'Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.';
@override
String get path_hexPrefixExample =>
'A1,F2,3C (każdy węzeł używa pierwszego bajtu swojego klucza publicznego)';
@override
String get path_labelHexPrefixes => 'Ścieżka (prefiksy hex)';
@override
String get path_helperMaxHops =>
'Maksymalnie 64 skoki. Każdy prefiks ma 2 znaki szesnastkowe (1 bajt).';
@override
String get path_selectFromContacts => 'Albo wybierz z kontaktów:';
@override
String get path_noRepeatersFound =>
'Nie znaleziono przekaźników ani serwerów pokoi.';
@override
String get path_customPathsRequire =>
'Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Nieprawidłowe prefiksy szesnastkowe: $prefixes';
}
@override
String get path_tooLong =>
'Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.';
@override
String get path_setPath => 'Ustaw Ścieżkę';
@override
String get repeater_management => 'Zarządzanie przekaźnikami';
@@ -2193,16 +2227,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_routingMode => 'Tryb routingu';
@override
String get repeater_autoUseSavedPath =>
'Automatycznie (użyj zapisanej ścieżki)';
@override
String get repeater_forceFloodMode => 'Wymuś tryb zalewowy';
@override
String get repeater_pathManagement => 'Zarządzanie ścieżkami';
@override
String get repeater_refresh => 'Odśwież';
@@ -3300,6 +3324,139 @@ class AppLocalizationsPl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Wejście cyfrowe';
@override
String get telemetry_digitalOutputLabel => 'Wyjście cyfrowe';
@override
String get telemetry_analogInputLabel => 'Wejście analogowe';
@override
String get telemetry_analogOutputLabel => 'Wyjście analogowe';
@override
String get telemetry_genericLabel => 'Czujnik ogólny';
@override
String get telemetry_luminosityLabel => 'Jasność';
@override
String get telemetry_presenceLabel => 'Obecność';
@override
String get telemetry_humidityLabel => 'Wilgotność';
@override
String get telemetry_accelerometerLabel => 'Akcelerometr';
@override
String get telemetry_pressureLabel => 'Ciśnienie';
@override
String get telemetry_altitudeLabel => 'Wysokość';
@override
String get telemetry_frequencyLabel => 'Częstotliwość';
@override
String get telemetry_percentageLabel => 'Procent';
@override
String get telemetry_concentrationLabel => 'Stężenie';
@override
String get telemetry_powerLabel => 'Moc';
@override
String get telemetry_distanceLabel => 'Odległość';
@override
String get telemetry_energyLabel => 'Energia';
@override
String get telemetry_directionLabel => 'Kierunek';
@override
String get telemetry_timeLabel => 'Czas';
@override
String get telemetry_gyrometerLabel => 'Żyrometr';
@override
String get telemetry_colourLabel => 'Kolor';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Przełącznik';
@override
String get telemetry_polylineLabel => 'Polilinia';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Liczba żądań';
@override
String get telemetry_error => 'Nie udało się pobrać danych';
@override
String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa';
@@ -4332,4 +4489,28 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Przybliż';
@override
String get map_zoomOut => 'Przybliż z powrotem';
@override
String get map_centerMap => 'Mapa centrum';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth wymaga przeglądarki Chromium.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS potwierdzone';
@override
String get pathTrace_legendInferred => 'Wywnioskowana pozycja';
}
+320 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_disable => 'Desativar';
@override
String get common_undo => 'Desfazer';
@override
String get messageStatus_sent => 'Enviado';
@override
String get messageStatus_delivered => 'Entregue';
@override
String get messageStatus_pending => 'Enviar';
@override
String get messageStatus_failed => 'Falhou ao enviar';
@override
String get messageStatus_repeated => 'Ouvi repetidamente';
@override
String get common_reboot => 'Reiniciar';
@@ -111,6 +129,12 @@ class AppLocalizationsPt extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Atualização automática';
@override
String get common_interval => 'Intervalo';
@override
String get scanner_title => 'MeshCore: Versão aberta';
@@ -799,11 +823,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Número de tentativas de reenvio antes de classificar uma mensagem como falha.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Bateria';
@@ -1005,6 +1024,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_newGroup => 'Novo Grupo';
@override
String get contacts_moreOptions => 'Mais opções';
@override
String get contacts_searchOpen => 'Pesquisar contatos';
@override
String get contacts_searchClose => 'Pesquisa avançada';
@override
String get contacts_groupName => 'Nome do grupo';
@@ -1484,34 +1512,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Espaço Hexadecimal:';
@override
String get chat_pathManagement => 'Gerenciamento de Caminhos';
@override
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
@override
String get chat_routingMode => 'Modo de roteamento';
@override
String get chat_autoUseSavedPath => 'Auto (usar caminho salvo)';
@override
String get chat_forceFloodMode => 'Modo de Inundação Forçado';
@override
String get chat_recentAckPaths => 'Rotas de ACK Recentes (toque para usar):';
@override
String get chat_pathHistoryFull =>
'O histórico está cheio. Remova entradas para adicionar novas.';
@override
String get chat_hopSingular => 'pule';
@override
String get chat_hopPlural => 'salta';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1523,12 +1523,6 @@ class AppLocalizationsPt extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Sucessos';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Remover caminho';
@@ -1536,53 +1530,148 @@ class AppLocalizationsPt extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.';
@override
String get chat_pathActions => 'Ações do Caminho:';
@override
String get chat_setCustomPath => 'Definir Caminho Personalizado';
@override
String get chat_setCustomPathSubtitle =>
'Especifique manualmente o caminho de roteamento';
@override
String get chat_clearPath => 'Limpar Caminho';
@override
String get chat_clearPathSubtitle =>
'Forçar a descoberta na próxima transmissão';
@override
String get chat_pathCleared =>
'Caminho limpo. A próxima mensagem redescobrirá a rota.';
@override
String get chat_floodModeSubtitle =>
'Use a chave de roteamento na barra de ferramentas';
@override
String get chat_floodModeEnabled =>
'Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.';
@override
String get chat_fullPath => 'Caminho Completo';
@override
String get chat_pathDetailsNotAvailable =>
'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.';
String get routing_title => 'Rotas';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Caminho definido: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Carro';
@override
String get routing_modeFlood => 'Inundação';
@override
String get routing_modeManual => 'Manual';
@override
String get routing_modeAutoHint =>
'Seleciona automaticamente o caminho mais conhecido, e, se nenhum caminho conhecido for encontrado, utiliza a estratégia de \"inundação\".';
@override
String get routing_modeFloodHint =>
'Transmissão através de todos os repetidores. É a opção mais confiável, mas utiliza mais tempo de transmissão.';
@override
String get routing_modeManualHint =>
'Sempre segue exatamente o caminho que você define.';
@override
String get routing_currentRoute => 'Rota atual';
@override
String get routing_directNoHops => 'Direto sem saltos de repetidor';
@override
String get routing_noPathYet =>
'Ainda não há um caminho definido. A mensagem continua a ser enviada até que uma rota seja encontrada.';
@override
String get routing_floodBroadcast =>
'Transmissão através de todos os repetidores';
@override
String get routing_editPath => 'Editar caminho';
@override
String get routing_forgetPath => 'Esqueça o caminho';
@override
String get routing_knownPaths => 'Rotas conhecidas';
@override
String get routing_knownPathsHint =>
'Toque em um caminho para alternar para ele.';
@override
String get routing_inUse => 'Em uso';
@override
String get routing_qualityStrong => 'Primeiro salto notável';
@override
String get routing_qualityGood => 'Primeiro salto bem-sucedido';
@override
String get routing_qualityFair => 'Primeira etapa bem-sucedida';
@override
String get routing_qualityWorked => 'Foi entregue';
@override
String get routing_qualityFlood =>
'Informação obtida através de relatos generalizados.';
@override
String get routing_qualityUntested => 'Não testado';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'nunca confirmado';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery =>
'Entrega em áreas afetadas por inundações';
@override
String get pathEditor_title => 'Criar Caminho';
@override
String pathEditor_hopCounter(int count) {
return '$count de 64 gramas de lúpulo';
}
@override
String get pathEditor_noHops =>
'Ainda não há lúpulos adicionados. Clique nos repetidores abaixo para adicioná-los na ordem desejada, ou salve sem adicionar lúpulos para enviar diretamente.';
@override
String get pathEditor_addHops => 'Adicione os lúpulos na seguinte ordem.';
@override
String get pathEditor_searchRepeaters => 'Encontrar repetidores';
@override
String get pathEditor_advancedHex => 'Avançado: caminho hexadecimal bruto';
@override
String get pathEditor_hexLabel => 'Prefixos hexadecimais';
@override
String get pathEditor_hexHelper =>
'Dois caracteres hexadecimais por salto, separados por vírgulas.';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Inválido: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Máximo de 64 saltos';
@override
String get pathEditor_usePath => 'Utilize este caminho.';
@override
String get pathEditor_removeHop => 'Remova o lúpulo';
@override
String get pathEditor_unknownHop => 'Repetidor desconhecido';
@override
String get chat_pathSavedLocally =>
'Salvo localmente. Conectar para sincronizar.';
@@ -2050,66 +2139,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_clear => 'Limpar';
@override
String path_currentPath(String path) {
return 'Caminho atual: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Usando $count $_temp0 caminho';
}
@override
String get path_enterCustomPath => 'Insira Caminho Personalizado';
@override
String get path_currentPathLabel => 'Caminho atual';
@override
String get path_hexPrefixInstructions =>
'Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.';
@override
String get path_hexPrefixExample =>
'A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)';
@override
String get path_labelHexPrefixes => 'Prefixo Hexadecimal';
@override
String get path_helperMaxHops =>
'Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)';
@override
String get path_selectFromContacts => 'Ou selecione de contatos:';
@override
String get path_noRepeatersFound =>
'Não foram encontrados repetidores ou servidores de sala.';
@override
String get path_customPathsRequire =>
'Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Prefixos hexadecimais inválidos: $prefixes';
}
@override
String get path_tooLong =>
'Caminho muito longo. Máximo de 64 saltos permitidos.';
@override
String get path_setPath => 'Definir Caminho';
@override
String get repeater_management => 'Gerenciamento de Repetidor';
@@ -2174,15 +2210,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_routingMode => 'Modo de roteamento';
@override
String get repeater_autoUseSavedPath => 'Auto (usar caminho salvo)';
@override
String get repeater_forceFloodMode => 'Modo de Inundação Forçado';
@override
String get repeater_pathManagement => 'Gerenciamento de caminhos';
@override
String get repeater_refresh => 'Atualizar';
@@ -3281,6 +3308,139 @@ class AppLocalizationsPt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Entrada digital';
@override
String get telemetry_digitalOutputLabel => 'Saída digital';
@override
String get telemetry_analogInputLabel => 'Entrada analógica';
@override
String get telemetry_analogOutputLabel => 'Saída analógica';
@override
String get telemetry_genericLabel => 'Sensor genérico';
@override
String get telemetry_luminosityLabel => 'Luminosidade';
@override
String get telemetry_presenceLabel => 'Presença';
@override
String get telemetry_humidityLabel => 'Humidade';
@override
String get telemetry_accelerometerLabel => 'Acelerómetro';
@override
String get telemetry_pressureLabel => 'Pressão';
@override
String get telemetry_altitudeLabel => 'Altitude';
@override
String get telemetry_frequencyLabel => 'Frequência';
@override
String get telemetry_percentageLabel => 'Percentagem';
@override
String get telemetry_concentrationLabel => 'Concentração';
@override
String get telemetry_powerLabel => 'Potência';
@override
String get telemetry_distanceLabel => 'Distância';
@override
String get telemetry_energyLabel => 'Energia';
@override
String get telemetry_directionLabel => 'Direção';
@override
String get telemetry_timeLabel => 'Hora';
@override
String get telemetry_gyrometerLabel => 'Girómetro';
@override
String get telemetry_colourLabel => 'Cor';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Interruptor';
@override
String get telemetry_polylineLabel => 'Polilinha';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Número de solicitações';
@override
String get telemetry_error => 'Não foi possível obter os dados';
@override
String get neighbors_receivedData => 'Dados dos Vizinhos Recebidos';
@@ -4308,4 +4468,28 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Ampliar';
@override
String get map_zoomOut => 'Ampliar';
@override
String get map_centerMap => 'Mapa do centro';
@override
String get chrome_bluetoothRequiresChromium =>
'O Web Bluetooth requer um navegador Chromium.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS confirmado';
@override
String get pathTrace_legendInferred => 'Posição inferida';
}
+322 -140
View File
@@ -92,6 +92,24 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get common_disable => 'Выключить';
@override
String get common_undo => 'Отменить';
@override
String get messageStatus_sent => 'Отправлено';
@override
String get messageStatus_delivered => 'Доставлено';
@override
String get messageStatus_pending => 'Отправка';
@override
String get messageStatus_failed => 'Не удалось отправить';
@override
String get messageStatus_repeated => 'Услышал несколько раз';
@override
String get common_reboot => 'Перезагрузить';
@@ -111,6 +129,12 @@ class AppLocalizationsRu extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Автообновление';
@override
String get common_interval => 'Интервал';
@override
String get scanner_title => 'MeshCore Open';
@@ -801,11 +825,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батарея';
@@ -1006,6 +1025,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contacts_newGroup => 'Новая группа';
@override
String get contacts_moreOptions => 'Больше вариантов';
@override
String get contacts_searchOpen => 'Найти контакты';
@override
String get contacts_searchClose => 'Закрыть поиск';
@override
String get contacts_groupName => 'Имя группы';
@@ -1485,35 +1513,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Шестнадцатеричный дамп:';
@override
String get chat_pathManagement => 'Управление маршрутами';
@override
String get chat_ShowAllPaths => 'Показать все пути';
@override
String get chat_routingMode => 'Режим маршрутизации';
@override
String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)';
@override
String get chat_forceFloodMode => 'Принудительный режим рассылки';
@override
String get chat_recentAckPaths =>
'Недавние подтверждённые маршруты (нажмите, чтобы использовать):';
@override
String get chat_pathHistoryFull =>
'История маршрутов заполнена. Удалите записи, чтобы добавить новые.';
@override
String get chat_hopSingular => 'хоп';
@override
String get chat_hopPlural => 'хопов';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1527,12 +1526,6 @@ class AppLocalizationsRu extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'успешно';
@override
String get chat_score => 'Оценка';
@override
String get chat_removePath => 'Удалить маршрут';
@@ -1540,54 +1533,150 @@ class AppLocalizationsRu extends AppLocalizations {
String get chat_noPathHistoryYet =>
'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.';
@override
String get chat_pathActions => 'Действия с маршрутом:';
@override
String get chat_setCustomPath => 'Указать маршрут вручную';
@override
String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи';
@override
String get chat_clearPath => 'Очистить маршрут';
@override
String get chat_clearPathSubtitle =>
'Принудительно обновить маршрут при следующей отправке';
@override
String get chat_pathCleared =>
'Маршрут очищен. Следующее сообщение обновит маршрут.';
@override
String get chat_floodModeSubtitle =>
'Используйте переключатель маршрутизации в панели приложения';
@override
String get chat_floodModeEnabled =>
'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.';
@override
String get chat_fullPath => 'Полный маршрут';
@override
String get chat_pathDetailsNotAvailable =>
'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.';
String get routing_title => 'Маршрутизация';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'хопов',
many: 'хопов',
few: 'хопа',
one: 'хоп',
);
return 'Маршрут установлен: $hopCount $_temp0$status';
String get routing_modeAuto => 'Авто';
@override
String get routing_modeFlood => 'Наводнение';
@override
String get routing_modeManual => 'Инструкция';
@override
String get routing_modeAutoHint =>
'Автоматически выбирает наиболее известный путь, и если такой путь неизвестен, использует алгоритм поиска пути.';
@override
String get routing_modeFloodHint =>
'Передача сигнала через все ретрансляторы. Самый надежный способ, но требует больше времени на передачу.';
@override
String get routing_modeManualHint =>
'Всегда следует точно по указанному вами маршруту.';
@override
String get routing_currentRoute => 'Текущий маршрут';
@override
String get routing_directNoHops =>
'Прямое соединение – без использования ретрансляторов';
@override
String get routing_noPathYet =>
'Пока нет пути. Следующее сообщение будет отправлено до тех пор, пока не будет обнаружен маршрут.';
@override
String get routing_floodBroadcast => 'Транслируется через все ретрансляторы';
@override
String get routing_editPath => 'Изменить путь';
@override
String get routing_forgetPath => 'Забудьте о маршруте';
@override
String get routing_knownPaths => 'Известные маршруты';
@override
String get routing_knownPathsHint =>
'Создайте маршрут для переключения на этот пункт.';
@override
String get routing_inUse => 'В эксплуатации';
@override
String get routing_qualityStrong => 'Сильный первый скачок';
@override
String get routing_qualityGood => 'Хорошее начало';
@override
String get routing_qualityFair => 'Первый хороший урожай';
@override
String get routing_qualityWorked => 'Осуществлено';
@override
String get routing_qualityFlood =>
'Узнал из новостей, распространяющихся в интернете.';
@override
String get routing_qualityUntested => 'Непроверенный';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'никогда не было подтверждено';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Доставка при затоплении';
@override
String get pathEditor_title => 'Создать маршрут';
@override
String pathEditor_hopCounter(int count) {
return '$count из 64 хмеля';
}
@override
String get pathEditor_noHops =>
'На данный момент хмель еще не добавлен. Чтобы добавить его, нажмите на соответствующие кнопки ниже в нужном порядке, или сохраните рецепт без хмеля, чтобы отправить его напрямую.';
@override
String get pathEditor_addHops =>
'Добавляйте хмель в соответствии с указанным порядком.';
@override
String get pathEditor_searchRepeaters => 'Поиск повторителей';
@override
String get pathEditor_advancedHex =>
'Продвинутый уровень: прямой путь в шестнадцатеричном формате';
@override
String get pathEditor_hexLabel => 'Префиксы шестнадцатеричной системы';
@override
String get pathEditor_hexHelper =>
'Два шестнадцатеричных символа на каждом шаге, разделенные запятыми.';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Неверно: $tokens';
}
@override
String get pathEditor_tooManyHops =>
'Максимальное количество ингредиентов – 64';
@override
String get pathEditor_usePath => 'Используйте этот путь';
@override
String get pathEditor_removeHop => 'Удалить хмель';
@override
String get pathEditor_unknownHop => 'Неизвестный ретранслятор';
@override
String get chat_pathSavedLocally =>
'Сохранено локально. Подключитесь для синхронизации.';
@@ -2055,66 +2144,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get common_clear => 'Очистить';
@override
String path_currentPath(String path) {
return 'Текущий маршрут: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'хопов',
many: 'хопов',
few: 'хопа',
one: 'хоп',
);
return 'Используется маршрут из $count $_temp0';
}
@override
String get path_enterCustomPath => 'Введите маршрут вручную';
@override
String get path_currentPathLabel => 'Текущий маршрут';
@override
String get path_hexPrefixInstructions =>
'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.';
@override
String get path_hexPrefixExample =>
'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)';
@override
String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)';
@override
String get path_helperMaxHops =>
'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)';
@override
String get path_selectFromContacts => 'Или выберите из контактов:';
@override
String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.';
@override
String get path_customPathsRequire =>
'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Недопустимые шестнадцатеричные префиксы: $prefixes';
}
@override
String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.';
@override
String get path_setPath => 'Установить маршрут';
@override
String get repeater_management => 'Управление репитером';
@@ -2179,16 +2214,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get repeater_routingMode => 'Режим маршрутизации';
@override
String get repeater_autoUseSavedPath =>
'Авто (использовать сохранённый маршрут)';
@override
String get repeater_forceFloodMode => 'Принудительный режим рассылки';
@override
String get repeater_pathManagement => 'Управление маршрутами';
@override
String get repeater_refresh => 'Обновить';
@@ -3289,6 +3314,139 @@ class AppLocalizationsRu extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Цифровой вход';
@override
String get telemetry_digitalOutputLabel => 'Цифровой выход';
@override
String get telemetry_analogInputLabel => 'Аналоговый вход';
@override
String get telemetry_analogOutputLabel => 'Аналоговый выход';
@override
String get telemetry_genericLabel => 'Общий датчик';
@override
String get telemetry_luminosityLabel => 'Освещённость';
@override
String get telemetry_presenceLabel => 'Присутствие';
@override
String get telemetry_humidityLabel => 'Влажность';
@override
String get telemetry_accelerometerLabel => 'Акселерометр';
@override
String get telemetry_pressureLabel => 'Давление';
@override
String get telemetry_altitudeLabel => 'Высота';
@override
String get telemetry_frequencyLabel => 'Частота';
@override
String get telemetry_percentageLabel => 'Процент';
@override
String get telemetry_concentrationLabel => 'Концентрация';
@override
String get telemetry_powerLabel => 'Мощность';
@override
String get telemetry_distanceLabel => 'Расстояние';
@override
String get telemetry_energyLabel => 'Энергия';
@override
String get telemetry_directionLabel => 'Направление';
@override
String get telemetry_timeLabel => 'Время';
@override
String get telemetry_gyrometerLabel => 'Гирометр';
@override
String get telemetry_colourLabel => 'Цвет';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Переключатель';
@override
String get telemetry_polylineLabel => 'Полилиния';
@override
String telemetry_altitudeValue(String meters) {
return '$meters м';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Гц';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa гПа';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux лк';
}
@override
String telemetry_powerValue(String watts) {
return '$watts Вт';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters м';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours кВт⋅ч';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Количество запросов';
@override
String get telemetry_error => 'Не удалось получить данные';
@override
String get neighbors_receivedData => 'Полученные данные о соседях';
@@ -4326,4 +4484,28 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contact_typeUnknown => 'Неизвестно';
@override
String get map_zoomIn => 'Увеличить масштаб';
@override
String get map_zoomOut => 'Увеличить масштаб';
@override
String get map_centerMap => 'Карта центра';
@override
String get chrome_bluetoothRequiresChromium =>
'Для работы Web Bluetooth требуется браузер на основе Chromium.';
@override
String channels_communityShortId(String id) {
return 'Идентификатор: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS подтверждено';
@override
String get pathTrace_legendInferred => 'Выведенная позиция';
}
+320 -137
View File
@@ -92,6 +92,24 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_disable => 'Zakázať';
@override
String get common_undo => 'Zrušiť';
@override
String get messageStatus_sent => 'Odoslané';
@override
String get messageStatus_delivered => 'Doručené';
@override
String get messageStatus_pending => 'Odoslanie';
@override
String get messageStatus_failed => 'Neúspešné odeslanie';
@override
String get messageStatus_repeated => 'Slyšal som to opakovane';
@override
String get common_reboot => 'Restartovať';
@@ -111,6 +129,12 @@ class AppLocalizationsSk extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatické obnovenie';
@override
String get common_interval => 'Časový interval';
@override
String get scanner_title => 'MeshCore Verzia pre verejnosť';
@@ -788,11 +812,6 @@ class AppLocalizationsSk extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Počet pokusov o odošleť pred označením správy ako neúspešnej';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batéria';
@@ -994,6 +1013,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_newGroup => 'Nová skupina';
@override
String get contacts_moreOptions => 'Ďalšie možnosti';
@override
String get contacts_searchOpen => 'Vyhľadajte kontakty';
@override
String get contacts_searchClose => 'Zavrieť vyhľadávanie';
@override
String get contacts_groupName => 'Názov skupiny';
@@ -1475,35 +1503,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Hexová analýza:';
@override
String get chat_pathManagement => 'Správa ciest';
@override
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
@override
String get chat_routingMode => 'Režim trasy';
@override
String get chat_autoUseSavedPath => 'Použiť uloženú cestu';
@override
String get chat_forceFloodMode =>
'Zavrieť režim núdzového povodňového režimu';
@override
String get chat_recentAckPaths => 'Nedávne cesty ACK (klepni na použitie):';
@override
String get chat_pathHistoryFull =>
'História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.';
@override
String get chat_hopSingular => 'Skok';
@override
String get chat_hopPlural => 'Skákať';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1515,12 +1514,6 @@ class AppLocalizationsSk extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Úspechy';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Odstrániť cestu';
@@ -1528,52 +1521,148 @@ class AppLocalizationsSk extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.';
@override
String get chat_pathActions => 'Cesty:';
@override
String get chat_setCustomPath => 'Nastaviť vlastnú cestu';
@override
String get chat_setCustomPathSubtitle => 'Ručne zadajte trasu.';
@override
String get chat_clearPath => 'Vyčistiš cestu';
@override
String get chat_clearPathSubtitle =>
'Znovu nájsť vynútene pri nasledujúcej pošlite';
@override
String get chat_pathCleared =>
'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.';
@override
String get chat_floodModeSubtitle =>
'Použite prepínanie trasy v navigačnom paneli.';
@override
String get chat_floodModeEnabled =>
'Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.';
@override
String get chat_fullPath => 'Celá cesta';
@override
String get chat_pathDetailsNotAvailable =>
'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.';
String get routing_title => 'Navigácia';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Cesta nastavená: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Auto';
@override
String get routing_modeFlood => 'Povodňová vlna';
@override
String get routing_modeManual => 'Ručná príručka';
@override
String get routing_modeAutoHint =>
'Automaticky vyberá najznámejší trasa, a ak žiadna nie je známa, použije náhodnú trasu.';
@override
String get routing_modeFloodHint =>
'Prenos prostredníctvom všetkých opakovačov. Najspoľahlivejší spôsob, ale vyžaduje viac času vysielania.';
@override
String get routing_modeManualHint =>
'Vždy dodáva presne podľa zadaného trasy.';
@override
String get routing_currentRoute => 'Aktuálna trasa';
@override
String get routing_directNoHops => 'Priamo bez prechodných trás';
@override
String get routing_noPathYet =>
'Zatiaľ neexistuje žiadna cesta. Nasledujúce správy budú pokračovať, kým sa nenájde trasa.';
@override
String get routing_floodBroadcast =>
'Prenos prostredníctvom každého opakovača';
@override
String get routing_editPath => 'Upraviť trasu';
@override
String get routing_forgetPath => 'Zabudnite na trasu';
@override
String get routing_knownPaths => 'Známe cesty';
@override
String get routing_knownPathsHint =>
'Kliknite na cestu, aby ste sa k nej presunuli.';
@override
String get routing_inUse => 'V prevádzke';
@override
String get routing_qualityStrong => 'Silný prvý krok';
@override
String get routing_qualityGood => 'Úspešný prvý krok';
@override
String get routing_qualityFair => 'Prvá, spravodlivá fáza';
@override
String get routing_qualityWorked => 'Dosiahnutý úspech';
@override
String get routing_qualityFlood =>
'Zistil som to z informácií, ktoré som získal v dôsledku povodňovej situácie.';
@override
String get routing_qualityUntested => 'Neotestované';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'nikedy nebolo potvrdené';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Doručenie v prípade povodní';
@override
String get pathEditor_title => 'Vytvorenie cesty';
@override
String pathEditor_hopCounter(int count) {
return '$count z 64 chmelových zŕš';
}
@override
String get pathEditor_noHops =>
'Zatiaľ žiadne chmel. Kliknite na opakované, aby ste ich pridali postupne, alebo uložte bez chmelu, aby ste ho mohli poslať priamo.';
@override
String get pathEditor_addHops => 'Pridávajte chmel podľa zadaného poriadku.';
@override
String get pathEditor_searchRepeaters => 'Hľadať opakované';
@override
String get pathEditor_advancedHex => 'Pokročilé: pôvodná hexová cesta';
@override
String get pathEditor_hexLabel => 'Prefiksy pre hexadecimálne čísla';
@override
String get pathEditor_hexHelper =>
'Dve hexové čísla na každý krok, oddelené čiarkami';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Neplatné: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Maximálne 64 krokov';
@override
String get pathEditor_usePath => 'Použite túto cestu';
@override
String get pathEditor_removeHop => 'Odstráňte chmel';
@override
String get pathEditor_unknownHop =>
'Neznáme zariadenie na opakované vysielanie';
@override
String get chat_pathSavedLocally =>
'Uložené lokálne. Spojte sa na synchronizáciu.';
@@ -2041,66 +2130,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_clear => 'Zmazať';
@override
String path_currentPath(String path) {
return 'Aktívna cesta: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Používa $count $_temp0 cestu';
}
@override
String get path_enterCustomPath => 'Zadajte vlastný priebeh';
@override
String get path_currentPathLabel => 'Aktuálny priebeh';
@override
String get path_hexPrefixInstructions =>
'Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.';
@override
String get path_hexPrefixExample =>
'A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)';
@override
String get path_labelHexPrefixes => 'Cesty (hexové predpony)';
@override
String get path_helperMaxHops =>
'Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).';
@override
String get path_selectFromContacts => 'Vyberte sa z kontaktov:';
@override
String get path_noRepeatersFound =>
'Nenašli sa žiadne opakovače ani serverové miestnosti.';
@override
String get path_customPathsRequire =>
'Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášať správky.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Neplatné hexové predpony: $prefixes';
}
@override
String get path_tooLong =>
'Cesta je príliš dlhá. Umožnené je maximum 64 skokov.';
@override
String get path_setPath => 'Nastaviť cestu';
@override
String get repeater_management => 'Správa opakérov';
@@ -2165,16 +2201,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_routingMode => 'Režim trasy';
@override
String get repeater_autoUseSavedPath => 'Použiť uloženú cestu';
@override
String get repeater_forceFloodMode =>
'Zavrieť režim núdzového povodňového režimu';
@override
String get repeater_pathManagement => 'Správa trás';
@override
String get repeater_refresh => 'Obnoviť';
@@ -3267,6 +3293,139 @@ class AppLocalizationsSk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Digitálny vstup';
@override
String get telemetry_digitalOutputLabel => 'Digitálny výstup';
@override
String get telemetry_analogInputLabel => 'Analógový vstup';
@override
String get telemetry_analogOutputLabel => 'Analógový výstup';
@override
String get telemetry_genericLabel => 'Všeobecný senzor';
@override
String get telemetry_luminosityLabel => 'Osvetlenie';
@override
String get telemetry_presenceLabel => 'Prítomnosť';
@override
String get telemetry_humidityLabel => 'Vlhkosť';
@override
String get telemetry_accelerometerLabel => 'Akcelerometer';
@override
String get telemetry_pressureLabel => 'Tlak';
@override
String get telemetry_altitudeLabel => 'Nadmorská výška';
@override
String get telemetry_frequencyLabel => 'Frekvencia';
@override
String get telemetry_percentageLabel => 'Percento';
@override
String get telemetry_concentrationLabel => 'Koncentrácia';
@override
String get telemetry_powerLabel => 'Výkon';
@override
String get telemetry_distanceLabel => 'Vzdialenosť';
@override
String get telemetry_energyLabel => 'Energia';
@override
String get telemetry_directionLabel => 'Smer';
@override
String get telemetry_timeLabel => 'Čas';
@override
String get telemetry_gyrometerLabel => 'Gyrometer';
@override
String get telemetry_colourLabel => 'Farba';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Prepínač';
@override
String get telemetry_polylineLabel => 'Lomená čiara';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Počet požiadaviek';
@override
String get telemetry_error => 'Nepodarilo sa získať údaje';
@override
String get neighbors_receivedData => 'Obdielo dáta suseda';
@@ -4291,4 +4450,28 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Zväčšiť';
@override
String get map_zoomOut => 'Zmenť zamer zblízka';
@override
String get map_centerMap => 'Mapa centra';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth vyžaduje prehliadač Chromium.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS potvrdilo';
@override
String get pathTrace_legendInferred => 'Odvodená poloha';
}
+317 -133
View File
@@ -92,6 +92,25 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_disable => 'Izklopiti';
@override
String get common_undo => 'Preobrn';
@override
String get messageStatus_sent => 'Pošljeno';
@override
String get messageStatus_delivered => 'Dostavljeno';
@override
String get messageStatus_pending => 'Pošiljanje';
@override
String get messageStatus_failed =>
'Uspešno ni bilo mogo, da se sporočilo pošlje';
@override
String get messageStatus_repeated => 'Slišal sem večkrat';
@override
String get common_reboot => 'Ponoviti';
@@ -111,6 +130,12 @@ class AppLocalizationsSl extends AppLocalizations {
return '$percent %';
}
@override
String get common_autoRefresh => 'Samodejno osveževanje';
@override
String get common_interval => 'Časovni interval';
@override
String get scanner_title => 'MeshCore Odprto';
@@ -789,11 +814,6 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Baterija';
@@ -993,6 +1013,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_newGroup => 'Nova skupina';
@override
String get contacts_moreOptions => 'Več možnosti';
@override
String get contacts_searchOpen => 'Iskanje kontaktov';
@override
String get contacts_searchClose => 'Izklopi iskanje';
@override
String get contacts_groupName => 'Ime skupine';
@@ -1472,34 +1501,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Izpis heksadecimalnih vrednosti:';
@override
String get chat_pathManagement => 'Upravljanje poti';
@override
String get chat_ShowAllPaths => 'Prikaži vse poti';
@override
String get chat_routingMode => 'Navodilo za usmerjevalni način';
@override
String get chat_autoUseSavedPath => 'Avto (uporabi shranjeno pot)';
@override
String get chat_forceFloodMode => 'Nasilje obvezati v način';
@override
String get chat_recentAckPaths => 'Nedavni poti ACK (tap za uporabo):';
@override
String get chat_pathHistoryFull =>
'Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.';
@override
String get chat_hopSingular => 'skok';
@override
String get chat_hopPlural => 'skokov';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1511,12 +1512,6 @@ class AppLocalizationsSl extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Uspešni';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Izbriši pot';
@@ -1524,51 +1519,144 @@ class AppLocalizationsSl extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.';
@override
String get chat_pathActions => 'Potni ukazi:';
@override
String get chat_setCustomPath => 'Nastavi Prilozeno Pot';
@override
String get chat_setCustomPathSubtitle => 'Ročno določite potniško pot.';
@override
String get chat_clearPath => 'Počisti pot';
@override
String get chat_clearPathSubtitle => 'Ob naslednji pošiljanju znova zbrati.';
@override
String get chat_pathCleared =>
'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.';
@override
String get chat_floodModeSubtitle =>
'Uporabi tipko usmerjevanja v meniju aplikacije.';
@override
String get chat_floodModeEnabled =>
'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.';
@override
String get chat_fullPath => 'Polna pot';
@override
String get chat_pathDetailsNotAvailable =>
'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.';
String get routing_title => 'Navigacija';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Pot nastavljen: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Avto';
@override
String get routing_modeFlood => 'Poplavo';
@override
String get routing_modeManual => 'Navodilo';
@override
String get routing_modeAutoHint =>
'Samodejno izbere najbolj poznano pot, in sicer, ko ni na voljo nobena.';
@override
String get routing_modeFloodHint =>
'Prenosi preko vseh repetitorjev. Najzanesljivejši način, vendar zahteva več časa.';
@override
String get routing_modeManualHint =>
'Vedno sledi natančni poti, ki jo ste določili.';
@override
String get routing_currentRoute => 'Trenutna pot';
@override
String get routing_directNoHops => 'Neposredno brez prehodov';
@override
String get routing_noPathYet =>
'Žep trenutno ni mogoče najti. Naslednje sporočilo bo posredovano, dokler ne bo ugotovljeno, kje je pot.';
@override
String get routing_floodBroadcast => 'Prenos preko vseh repetitiv';
@override
String get routing_editPath => 'Uredi pot';
@override
String get routing_forgetPath => 'Pozabi na pot';
@override
String get routing_knownPaths => 'Poznati poti';
@override
String get routing_knownPathsHint => 'Kliknite na pot, da jo izberete.';
@override
String get routing_inUse => 'V uporabi';
@override
String get routing_qualityStrong => 'Močan prvi korak';
@override
String get routing_qualityGood => 'Prva uspešna faza';
@override
String get routing_qualityFair => 'Prva, uspešna faza';
@override
String get routing_qualityWorked => 'Izpolnil';
@override
String get routing_qualityFlood => 'Slišano preko poplave';
@override
String get routing_qualityUntested => 'Ne preizkušen';
@override
String routing_lastWorked(String when) {
return 'delal/a $when';
}
@override
String get routing_neverWorked => 'nikoli ni bilo potrjeno';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Dostava zaradi poplave';
@override
String get pathEditor_title => 'Izgradnja poti';
@override
String pathEditor_hopCounter(int count) {
return '$count od 64 različnih sort hropa';
}
@override
String get pathEditor_noHops =>
'Še niso dodani hmelji. Za dodajanje hmelja v vrstnem redu kliknite na povezavo spodaj, ali pa shranite brez dodanega hmelja, da ga lahko posredujete neposredno.';
@override
String get pathEditor_addHops => 'Dodajte suho travo v skladu s postopkom.';
@override
String get pathEditor_searchRepeaters => 'Iskanje ponovitev';
@override
String get pathEditor_advancedHex => 'Napredno: surovi šestnajstni pot';
@override
String get pathEditor_hexLabel => 'Predfiks za heksadecimalno šifro';
@override
String get pathEditor_hexHelper =>
'Dva šestbitna znaka na vsak skok, ločena z vejico';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Neveljaven: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Največ 64 hopov';
@override
String get pathEditor_usePath => 'Uporabite to poto';
@override
String get pathEditor_removeHop => 'Odstranite hmelj';
@override
String get pathEditor_unknownHop => 'Neznani ponovitelj';
@override
String get chat_pathSavedLocally =>
'Shrano lokalno. Povežite se za sinhronizacijo.';
@@ -2038,65 +2126,13 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_clear => 'Ponoviti';
@override
String path_currentPath(String path) {
return 'Trenutna pot: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Uporablja $count $_temp0 pot';
}
@override
String get path_enterCustomPath => 'Vnesite prilagojeno pot';
@override
String get path_currentPathLabel => 'Trenutna pot';
@override
String get path_hexPrefixInstructions =>
'Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.';
@override
String get path_hexPrefixExample =>
'Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)';
@override
String get path_labelHexPrefixes => 'Pot (heksafixne skrajšave)';
@override
String get path_helperMaxHops =>
'Maksimalno 64 skokov. Vsak prefiks je 2 heksadecimalna znamenja (1 bajt).';
@override
String get path_selectFromContacts => 'Izberi iz kontaktov:';
@override
String get path_noRepeatersFound =>
'Ne najdenih ponoviteljev ali strežnikov sob.';
@override
String get path_customPathsRequire =>
'Prilojene poti zahtevajo medhodne prenose, ki lahko prenašajo sporočila.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Neveljačni šesteročlenski prefiksi: $prefixes';
}
@override
String get path_tooLong => 'Pot je prevelika. Dovoljeno največ 64 skokov.';
@override
String get path_setPath => 'Nastavi Pot';
@override
String get repeater_management => 'Upravljanje ponovitve';
@@ -2162,15 +2198,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get repeater_routingMode => 'Navodilo za usmerjevalni način';
@override
String get repeater_autoUseSavedPath => 'Avto (uporabi shranjeno pot)';
@override
String get repeater_forceFloodMode => 'Nasilje obvezati v način';
@override
String get repeater_pathManagement => 'Upravljanje poti';
@override
String get repeater_refresh => 'Ponovno obnavljati';
@@ -3262,6 +3289,139 @@ class AppLocalizationsSl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Digitalni vhod';
@override
String get telemetry_digitalOutputLabel => 'Digitalni izhod';
@override
String get telemetry_analogInputLabel => 'Analogni vhod';
@override
String get telemetry_analogOutputLabel => 'Analogni izhod';
@override
String get telemetry_genericLabel => 'Splošni senzor';
@override
String get telemetry_luminosityLabel => 'Osvetljenost';
@override
String get telemetry_presenceLabel => 'Prisotnost';
@override
String get telemetry_humidityLabel => 'Vlažnost';
@override
String get telemetry_accelerometerLabel => 'Merilnik pospeška';
@override
String get telemetry_pressureLabel => 'Tlak';
@override
String get telemetry_altitudeLabel => 'Nadmorska višina';
@override
String get telemetry_frequencyLabel => 'Frekvenca';
@override
String get telemetry_percentageLabel => 'Odstotek';
@override
String get telemetry_concentrationLabel => 'Koncentracija';
@override
String get telemetry_powerLabel => 'Moč';
@override
String get telemetry_distanceLabel => 'Razdalja';
@override
String get telemetry_energyLabel => 'Energija';
@override
String get telemetry_directionLabel => 'Smer';
@override
String get telemetry_timeLabel => 'Čas';
@override
String get telemetry_gyrometerLabel => 'Žiroskop';
@override
String get telemetry_colourLabel => 'Barva';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Stikalo';
@override
String get telemetry_polylineLabel => 'Polilinija';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Število zahtev';
@override
String get telemetry_error => 'Podatkov ni bilo mogoče pridobiti';
@override
String get neighbors_receivedData => 'Prejeto podatke o sosedih';
@@ -4289,4 +4449,28 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Povečaj';
@override
String get map_zoomOut => 'Povečajte pogled';
@override
String get map_centerMap => 'Krajšarska karta';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth zahteva brskalnik Chromium.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS potrdilo';
@override
String get pathTrace_legendInferred => 'Izpeljana lokacija';
}
+316 -133
View File
@@ -92,6 +92,24 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_disable => 'Inaktivera';
@override
String get common_undo => 'Ångra';
@override
String get messageStatus_sent => 'Sen';
@override
String get messageStatus_delivered => 'Levererad';
@override
String get messageStatus_pending => 'Skicka';
@override
String get messageStatus_failed => 'Misslyckades med att skicka';
@override
String get messageStatus_repeated => 'Hördes upprepade gånger';
@override
String get common_reboot => 'Start om';
@@ -111,6 +129,12 @@ class AppLocalizationsSv extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatisk uppdatering';
@override
String get common_interval => 'Intervall';
@override
String get scanner_title => 'MeshCore Öppen version';
@@ -783,11 +807,6 @@ class AppLocalizationsSv extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batteri';
@@ -988,6 +1007,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_newGroup => 'Ny grupp';
@override
String get contacts_moreOptions => 'Fler alternativ';
@override
String get contacts_searchOpen => 'Sök efter kontakter';
@override
String get contacts_searchClose => 'Avancerad sökning';
@override
String get contacts_groupName => 'Gruppnamn';
@@ -1466,35 +1494,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Hexdump:';
@override
String get chat_pathManagement => 'Stigarhantering';
@override
String get chat_ShowAllPaths => 'Visa alla vägar';
@override
String get chat_routingMode => 'Ruttläge';
@override
String get chat_autoUseSavedPath => 'Automatisk (använd sparad sökväg)';
@override
String get chat_forceFloodMode => 'Tvinga Översvämningsläge';
@override
String get chat_recentAckPaths =>
'Nyligen Ack-vägar (tryck för att använda):';
@override
String get chat_pathHistoryFull =>
'Historisk sökväg är full. Ta bort poster för att lägga till nya.';
@override
String get chat_hopSingular => 'hoppa';
@override
String get chat_hopPlural => 'hoppar';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1506,12 +1505,6 @@ class AppLocalizationsSv extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'framgångar';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Ta bort sökväg';
@@ -1519,50 +1512,144 @@ class AppLocalizationsSv extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.';
@override
String get chat_pathActions => 'Stigar:';
@override
String get chat_setCustomPath => 'Ange anpassad sökväg';
@override
String get chat_setCustomPathSubtitle => 'Ange ruttväg manuellt';
@override
String get chat_clearPath => 'Rensa Vägen';
@override
String get chat_clearPathSubtitle => 'Tvinga fram omstart vid nästa sändning';
@override
String get chat_pathCleared =>
'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.';
@override
String get chat_floodModeSubtitle => 'Använd routningsomkopplaren i appraden';
@override
String get chat_floodModeEnabled =>
'Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.';
@override
String get chat_fullPath => 'Fullständig sökväg';
@override
String get chat_pathDetailsNotAvailable =>
'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.';
String get routing_title => 'Ruttplanering';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'hoppar',
one: 'hopp',
);
return 'Sökväg inställd: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Bil';
@override
String get routing_modeFlood => 'Översvämning';
@override
String get routing_modeManual => 'Instruktioner';
@override
String get routing_modeAutoHint =>
'Väljer automatiskt den bästa kända vägen, och använder en \"flooding\"-strategi om ingen väg är känd.';
@override
String get routing_modeFloodHint =>
'Sändningar via alla repetrar. Det mest pålitliga alternativet, men kräver mer sändtid.';
@override
String get routing_modeManualHint =>
'Skickar alltid den exakta väg du har angivit.';
@override
String get routing_currentRoute => 'Nuvarande rutt';
@override
String get routing_directNoHops => 'Direkt utan mellanliggande routrar';
@override
String get routing_noPathYet =>
'Ingen väg hittad ännu. Nästa meddelande skickas tills en rutt har upptäckts.';
@override
String get routing_floodBroadcast => 'Sändas via alla repetrar';
@override
String get routing_editPath => 'Redigera sökväg';
@override
String get routing_forgetPath => 'Glöm vägen';
@override
String get routing_knownPaths => 'Kända vägar';
@override
String get routing_knownPathsHint => 'Välj en väg för att byta till den.';
@override
String get routing_inUse => 'I användning';
@override
String get routing_qualityStrong => 'En stark start';
@override
String get routing_qualityGood => 'Bra första steg';
@override
String get routing_qualityFair => 'Bra första hopp';
@override
String get routing_qualityWorked => 'Har levererat';
@override
String get routing_qualityFlood => 'Fått information via nyhetsflöde';
@override
String get routing_qualityUntested => 'Ej testat';
@override
String routing_lastWorked(String when) {
return 'arbetade $when';
}
@override
String get routing_neverWorked => 'aldrig bekräftat';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Leverans vid översvämningsområde';
@override
String get pathEditor_title => 'Skapa väg';
@override
String pathEditor_hopCounter(int count) {
return '$count av 64 humlor';
}
@override
String get pathEditor_noHops =>
'Inga humle än. Använd knapparna nedan för att lägga till dem i rätt ordning, eller spara utan humle för att skicka direkt.';
@override
String get pathEditor_addHops => 'Tillsätt humlen i rätt ordning.';
@override
String get pathEditor_searchRepeaters => 'Sök efter återupptagna samtal';
@override
String get pathEditor_advancedHex => 'Avancerat: rå hex-sökväg';
@override
String get pathEditor_hexLabel => 'Hex-prefikser';
@override
String get pathEditor_hexHelper =>
'Två hex-tecken per steg, separerade med kommatecken.';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Ogiltigt: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Maximalt 64 humlörter';
@override
String get pathEditor_usePath => 'Använd denna väg';
@override
String get pathEditor_removeHop => 'Ta bort humlen';
@override
String get pathEditor_unknownHop => 'Okänd förstärkare';
@override
String get chat_pathSavedLocally =>
'Sparat lokalt. Anslut för att synkronisera.';
@@ -2027,65 +2114,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_clear => 'Rensa';
@override
String path_currentPath(String path) {
return 'Nuvarande sökväg: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
);
return 'Använda $count $_temp0 sökväg';
}
@override
String get path_enterCustomPath => 'Ange anpassad sökväg';
@override
String get path_currentPathLabel => 'Nuvarande sökväg';
@override
String get path_hexPrefixInstructions =>
'Ange 2-tecknets hex-prefett för varje hopp, åtskilda med komma.';
@override
String get path_hexPrefixExample =>
'Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)';
@override
String get path_labelHexPrefixes => 'Hexprefixer';
@override
String get path_helperMaxHops =>
'Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)';
@override
String get path_selectFromContacts => 'Välj istället från kontakter:';
@override
String get path_noRepeatersFound =>
'Inga återuppspelare eller rumsservrar hittades.';
@override
String get path_customPathsRequire =>
'Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Ogiltiga hex-prefikser: $prefixes';
}
@override
String get path_tooLong => 'Sökvägen är för lång. Max 64 hopp tillåtna.';
@override
String get path_setPath => 'Ange Sökväg';
@override
String get repeater_management => 'Återuppspelarens Hantering';
@@ -2150,15 +2185,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_routingMode => 'Ruttläge';
@override
String get repeater_autoUseSavedPath => 'Automatisk (använd sparad sökväg)';
@override
String get repeater_forceFloodMode => 'Tvinga Översvämningsläge';
@override
String get repeater_pathManagement => 'Stigarhantering';
@override
String get repeater_refresh => 'Uppdatera';
@@ -3243,6 +3269,139 @@ class AppLocalizationsSv extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Digital ingång';
@override
String get telemetry_digitalOutputLabel => 'Digital utgång';
@override
String get telemetry_analogInputLabel => 'Analog ingång';
@override
String get telemetry_analogOutputLabel => 'Analog utgång';
@override
String get telemetry_genericLabel => 'Allmän sensor';
@override
String get telemetry_luminosityLabel => 'Ljusstyrka';
@override
String get telemetry_presenceLabel => 'Närvaro';
@override
String get telemetry_humidityLabel => 'Luftfuktighet';
@override
String get telemetry_accelerometerLabel => 'Accelerometer';
@override
String get telemetry_pressureLabel => 'Tryck';
@override
String get telemetry_altitudeLabel => 'Höjd';
@override
String get telemetry_frequencyLabel => 'Frekvens';
@override
String get telemetry_percentageLabel => 'Procent';
@override
String get telemetry_concentrationLabel => 'Koncentration';
@override
String get telemetry_powerLabel => 'Effekt';
@override
String get telemetry_distanceLabel => 'Avstånd';
@override
String get telemetry_energyLabel => 'Energi';
@override
String get telemetry_directionLabel => 'Riktning';
@override
String get telemetry_timeLabel => 'Tid';
@override
String get telemetry_gyrometerLabel => 'Gyrometer';
@override
String get telemetry_colourLabel => 'Färg';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Brytare';
@override
String get telemetry_polylineLabel => 'Polylinje';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Antal förfrågningar';
@override
String get telemetry_error => 'Det gick inte att hämta data';
@override
String get neighbors_receivedData => 'Mottagna grannars data';
@@ -4264,4 +4423,28 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => 'Zooma in';
@override
String get map_zoomOut => 'Zooma ut';
@override
String get map_centerMap => 'Kartöversikt';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth kräver en Chromium-baserad webbläsare.';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS-verifierat';
@override
String get pathTrace_legendInferred => 'Antagen position';
}
+320 -140
View File
@@ -92,6 +92,24 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get common_disable => 'Вимкнути';
@override
String get common_undo => 'Скасувати';
@override
String get messageStatus_sent => 'Надіслано';
@override
String get messageStatus_delivered => 'Доставлено';
@override
String get messageStatus_pending => 'Надсилання';
@override
String get messageStatus_failed => 'Не вдалося надіслати';
@override
String get messageStatus_repeated => 'Почув неодноразово';
@override
String get common_reboot => 'Перезавантажити';
@@ -111,6 +129,12 @@ class AppLocalizationsUk extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Автооновлення';
@override
String get common_interval => 'Інтервал';
@override
String get scanner_title => 'MeshCore: Відкритий доступ';
@@ -795,11 +819,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle =>
'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батарея';
@@ -1001,6 +1020,15 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contacts_newGroup => 'Нова група';
@override
String get contacts_moreOptions => 'Більше можливостей';
@override
String get contacts_searchOpen => 'Пошук контактів';
@override
String get contacts_searchClose => 'Закрити пошук';
@override
String get contacts_groupName => 'Назва групи';
@@ -1480,35 +1508,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get debugFrame_hexDump => 'Дамп Hex:';
@override
String get chat_pathManagement => 'Керування шляхами';
@override
String get chat_ShowAllPaths => 'Показати всі шляхи';
@override
String get chat_routingMode => 'Режим маршрутизації';
@override
String get chat_autoUseSavedPath => 'Авто (використовувати збережений шлях)';
@override
String get chat_forceFloodMode => 'Примусово через всю мережу';
@override
String get chat_recentAckPaths =>
'Підтверджені шляхи (натисніть, щоб використати):';
@override
String get chat_pathHistoryFull =>
'Історія шляхів заповнена. Видаліть записи, щоб додати нові.';
@override
String get chat_hopSingular => 'Перехід';
@override
String get chat_hopPlural => 'переходів';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
@@ -1522,12 +1521,6 @@ class AppLocalizationsUk extends AppLocalizations {
return '$count $_temp0';
}
@override
String get chat_successes => 'Успішно';
@override
String get chat_score => 'Оцінка';
@override
String get chat_removePath => 'Видалити шлях';
@@ -1535,54 +1528,148 @@ class AppLocalizationsUk extends AppLocalizations {
String get chat_noPathHistoryYet =>
'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.';
@override
String get chat_pathActions => 'Дії зі шляхом:';
@override
String get chat_setCustomPath => 'Встановити власний шлях';
@override
String get chat_setCustomPathSubtitle => 'Вказати шлях маршрутизації вручну';
@override
String get chat_clearPath => 'Очистити шлях';
@override
String get chat_clearPathSubtitle =>
'Примусово повторити пошук при наступному надсиланні';
@override
String get chat_pathCleared =>
'Шлях очищено. Наступне повідомлення оновить маршрут.';
@override
String get chat_floodModeSubtitle =>
'Використовувати перемикач маршрутизації в панелі застосунку';
@override
String get chat_floodModeEnabled =>
'Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.';
@override
String get chat_fullPath => 'Повний шлях';
@override
String get chat_pathDetailsNotAvailable =>
'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.';
String get routing_title => 'Маршрутизація';
@override
String chat_pathSetHops(int hopCount, String status) {
String _temp0 = intl.Intl.pluralLogic(
hopCount,
locale: localeName,
other: 'переходів',
many: 'переходів',
few: 'переходи',
one: 'перехід',
);
return 'Шлях встановлено: $hopCount $_temp0 - $status';
String get routing_modeAuto => 'Автомобіль';
@override
String get routing_modeFlood => 'Повені';
@override
String get routing_modeManual => 'Інструкція';
@override
String get routing_modeAutoHint =>
'Автоматично обирає найкращий відомий шлях, та у разі відсутності відомого шляху, використовує алгоритм \"занурення\".';
@override
String get routing_modeFloodHint =>
'Передавання через усі ретранслятори. Найбільш надійний спосіб, але потребує більше часу.';
@override
String get routing_modeManualHint =>
'Завжди доставляє точно за вказаним вами маршрутом.';
@override
String get routing_currentRoute => 'Поточний маршрут';
@override
String get routing_directNoHops =>
'Пряме з\'єднання – без проміжних ретрансляторів';
@override
String get routing_noPathYet =>
'Поки що немає жодного шляху. Повідомлення продовжуються надходити, поки не буде знайдено маршрут.';
@override
String get routing_floodBroadcast => 'Поширення через усі ретранслятори';
@override
String get routing_editPath => 'Редагувати шлях';
@override
String get routing_forgetPath => 'Забудь про шлях';
@override
String get routing_knownPaths => 'Відомі маршрути';
@override
String get routing_knownPathsHint =>
'Виберіть опцію, щоб переключитися на неї.';
@override
String get routing_inUse => 'У робочому стані';
@override
String get routing_qualityStrong => 'Сильний перший стрибок';
@override
String get routing_qualityGood => 'Чудова перша спроба';
@override
String get routing_qualityFair => 'Перший, але вдалий, крок';
@override
String get routing_qualityWorked => 'Доставлено';
@override
String get routing_qualityFlood => 'Дізнався через новини';
@override
String get routing_qualityUntested => 'Не протестовано';
@override
String routing_lastWorked(String when) {
return 'worked $when';
}
@override
String get routing_neverWorked => 'ніколи не підтверджено';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => 'Доставка під час повені';
@override
String get pathEditor_title => 'Створити маршрут';
@override
String pathEditor_hopCounter(int count) {
return '$count з 64 штук хмелю';
}
@override
String get pathEditor_noHops =>
'Ще не додано хміль. Натисніть на відповідні кнопки, щоб додати його в потрібному порядку, або збережіть рецепт без хмілю, щоб відправити його безпосередньо.';
@override
String get pathEditor_addHops => 'Додавайте хміль у наступній послідовності.';
@override
String get pathEditor_searchRepeaters => 'Пошук повторювачів';
@override
String get pathEditor_advancedHex =>
'Просунутий рівень: пряма шлях у форматі шестнадцяткової системи.';
@override
String get pathEditor_hexLabel =>
'Префікси для шестнадцяткової системи числення';
@override
String get pathEditor_hexHelper =>
'Два шестизначні символи на кожний крок, розділені комами';
@override
String pathEditor_invalidTokens(String tokens) {
return 'Неправильно: $tokens';
}
@override
String get pathEditor_tooManyHops => 'Максимум 64 хмелеві колоди';
@override
String get pathEditor_usePath => 'Використовуйте цей шлях';
@override
String get pathEditor_removeHop => 'Видалити хміль';
@override
String get pathEditor_unknownHop => 'Невідомий ретранслятор';
@override
String get chat_pathSavedLocally =>
'Збережено локально. Підключіться для синхронізації.';
@@ -2049,67 +2136,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get common_clear => 'Очистити';
@override
String path_currentPath(String path) {
return 'Поточний шлях: $path';
}
@override
String path_usingHopsPath(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'переходами',
many: 'переходами',
few: 'переходами',
one: 'переходом',
);
return 'Використання шляху з $count $_temp0';
}
@override
String get path_enterCustomPath => 'Ввести власний шлях';
@override
String get path_currentPathLabel => 'Поточний шлях';
@override
String get path_hexPrefixInstructions =>
'Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.';
@override
String get path_hexPrefixExample =>
'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).';
@override
String get path_labelHexPrefixes => 'Hex-префікси';
@override
String get path_helperMaxHops =>
'Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)';
@override
String get path_selectFromContacts => 'Вибрати з контактів:';
@override
String get path_noRepeatersFound =>
'Ретрансляторів або серверів кімнат не знайдено.';
@override
String get path_customPathsRequire =>
'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.';
@override
String path_invalidHexPrefixes(String prefixes) {
return 'Некоректні hex-префікси: $prefixes';
}
@override
String get path_tooLong => 'Шлях занадто довгий. Максимум 64 переходи.';
@override
String get path_setPath => 'Встановити шлях';
@override
String get repeater_management => 'Керування ретранслятором';
@@ -2174,16 +2207,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get repeater_routingMode => 'Режим маршрутизації';
@override
String get repeater_autoUseSavedPath =>
'Авто (використовувати збережений шлях)';
@override
String get repeater_forceFloodMode => 'Примусово через всю мережу';
@override
String get repeater_pathManagement => 'Керування шляхами';
@override
String get repeater_refresh => 'Оновити';
@@ -3286,6 +3309,139 @@ class AppLocalizationsUk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => 'Цифровий вхід';
@override
String get telemetry_digitalOutputLabel => 'Цифровий вихід';
@override
String get telemetry_analogInputLabel => 'Аналоговий вхід';
@override
String get telemetry_analogOutputLabel => 'Аналоговий вихід';
@override
String get telemetry_genericLabel => 'Загальний датчик';
@override
String get telemetry_luminosityLabel => 'Освітленість';
@override
String get telemetry_presenceLabel => 'Присутність';
@override
String get telemetry_humidityLabel => 'Вологість';
@override
String get telemetry_accelerometerLabel => 'Акселерометр';
@override
String get telemetry_pressureLabel => 'Тиск';
@override
String get telemetry_altitudeLabel => 'Висота';
@override
String get telemetry_frequencyLabel => 'Частота';
@override
String get telemetry_percentageLabel => 'Відсоток';
@override
String get telemetry_concentrationLabel => 'Концентрація';
@override
String get telemetry_powerLabel => 'Потужність';
@override
String get telemetry_distanceLabel => 'Відстань';
@override
String get telemetry_energyLabel => 'Енергія';
@override
String get telemetry_directionLabel => 'Напрямок';
@override
String get telemetry_timeLabel => 'Час';
@override
String get telemetry_gyrometerLabel => 'Гірометр';
@override
String get telemetry_colourLabel => 'Колір';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => 'Перемикач';
@override
String get telemetry_polylineLabel => 'Полілінія';
@override
String telemetry_altitudeValue(String meters) {
return '$meters м';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Гц';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa гПа';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux лк';
}
@override
String telemetry_powerValue(String watts) {
return '$watts Вт';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters м';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours кВт⋅год';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => 'Кількість запитів';
@override
String get telemetry_error => 'Не вдалося отримати дані';
@override
String get neighbors_receivedData => 'Дані сусідів отримано';
@@ -4327,4 +4483,28 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contact_typeUnknown => 'Невідомо';
@override
String get map_zoomIn => 'Увійти в режим збільшення';
@override
String get map_zoomOut => 'Видалити зум';
@override
String get map_centerMap => 'Карта центру';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth вимагає браузера на основі Chromium';
@override
String channels_communityShortId(String id) {
return 'ID: $id...';
}
@override
String get pathTrace_legendGpsConfirmed => 'GPS підтверджено';
@override
String get pathTrace_legendInferred => 'Висновок щодо положення';
}
+311 -113
View File
@@ -92,6 +92,24 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get common_disable => '禁用';
@override
String get common_undo => '撤销';
@override
String get messageStatus_sent => '发送';
@override
String get messageStatus_delivered => '已送达';
@override
String get messageStatus_pending => '发送';
@override
String get messageStatus_failed => '发送失败';
@override
String get messageStatus_repeated => '多次听到';
@override
String get common_reboot => '重启';
@@ -111,6 +129,12 @@ class AppLocalizationsZh extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => '自动刷新';
@override
String get common_interval => '间隔';
@override
String get scanner_title => '连接设备';
@@ -741,11 +765,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => '电池';
@@ -937,6 +956,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_newGroup => '新建群聊';
@override
String get contacts_moreOptions => '更多选择';
@override
String get contacts_searchOpen => '搜索联系人';
@override
String get contacts_searchClose => '高级搜索';
@override
String get contacts_groupName => '群聊名称';
@@ -1403,85 +1431,149 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get debugFrame_hexDump => '十六进制数据:';
@override
String get chat_pathManagement => '路径管理';
@override
String get chat_ShowAllPaths => '显示所有路径';
@override
String get chat_routingMode => '路由模式';
@override
String get chat_autoUseSavedPath => '自动(使用保存的路径)';
@override
String get chat_forceFloodMode => '强制泛洪模式';
@override
String get chat_recentAckPaths => '最近使用的 ACK 路径(点击使用):';
@override
String get chat_pathHistoryFull => '路径历史已满,请删除后再添加。';
@override
String get chat_hopSingular => '';
@override
String get chat_hopPlural => '';
@override
String chat_hopsCount(int count) {
return '$count';
}
@override
String get chat_successes => '成功';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => '移除路径';
@override
String get chat_noPathHistoryYet => '暂无路径历史。\n发送消息以探索路径。';
@override
String get chat_pathActions => '路径操作:';
@override
String get chat_setCustomPath => '设置自定义路径';
@override
String get chat_setCustomPathSubtitle => '手动指定路由路径';
@override
String get chat_clearPath => '清除路径';
@override
String get chat_clearPathSubtitle => '清除当前路径,下次发送将重新尝试。';
@override
String get chat_pathCleared => '路径已清除。下一条消息将重新路由。';
@override
String get chat_floodModeSubtitle => '在应用栏中切换路由模式。';
@override
String get chat_floodModeEnabled => '泛洪模式已启用。可通过应用栏的路由图标切换。';
@override
String get chat_fullPath => '完整路径';
@override
String get chat_pathDetailsNotAvailable => '径信息暂不可用,请尝试发送消息刷新。';
String get routing_title => '';
@override
String chat_pathSetHops(int hopCount, String status) {
return '路径设置:$hopCount 跳 - $status';
String get routing_modeAuto => '汽车';
@override
String get routing_modeFlood => '洪水';
@override
String get routing_modeManual => '手册';
@override
String get routing_modeAutoHint => '自动选择已知最佳路径,当没有已知路径时,则进行“洪水”搜索。';
@override
String get routing_modeFloodHint => '通过所有中继站进行广播。 这种方式最可靠,但占用更多的时间。';
@override
String get routing_modeManualHint => '总是按照您设置的路径进行导航。';
@override
String get routing_currentRoute => '当前路线';
@override
String get routing_directNoHops => '直接连接— 无中继跳';
@override
String get routing_noPathYet => '目前还没有找到路径。直到找到路径,才会收到后续消息。';
@override
String get routing_floodBroadcast => '通过所有中继器进行广播';
@override
String get routing_editPath => '编辑路径';
@override
String get routing_forgetPath => '忘记原路';
@override
String get routing_knownPaths => '已知的路径';
@override
String get routing_knownPathsHint => '点击该路径以切换到它。';
@override
String get routing_inUse => '使用中';
@override
String get routing_qualityStrong => '强劲的初始阶段';
@override
String get routing_qualityGood => '不错的开端';
@override
String get routing_qualityFair => '第一次尝试,结果良好';
@override
String get routing_qualityWorked => '已完成';
@override
String get routing_qualityFlood => '通过新闻报道';
@override
String get routing_qualityUntested => '未经测试';
@override
String routing_lastWorked(String when) {
return '工作于 $when';
}
@override
String get routing_neverWorked => '从未得到证实';
@override
String routing_deliveryCounts(int successes, int failures) {
return '$successes delivered, $failures failed';
}
@override
String get routing_floodDelivery => '洪水配送';
@override
String get pathEditor_title => '构建路径';
@override
String pathEditor_hopCounter(int count) {
return '$count of 64 hops';
}
@override
String get pathEditor_noHops =>
'目前还没有添加任何啤酒花。点击下面的“添加”按钮,按顺序添加,或者直接保存,不添加任何啤酒花。';
@override
String get pathEditor_addHops => '按照顺序添加啤酒花';
@override
String get pathEditor_searchRepeaters => '重复搜索';
@override
String get pathEditor_advancedHex => '高级:原始十六进制路径';
@override
String get pathEditor_hexLabel => '十六进制前缀';
@override
String get pathEditor_hexHelper => '每次跳跃,使用两个十六进制字符,用逗号分隔。';
@override
String pathEditor_invalidTokens(String tokens) {
return '无效:$tokens';
}
@override
String get pathEditor_tooManyHops => '最多 64 个跳跃';
@override
String get pathEditor_usePath => '请使用此路径';
@override
String get pathEditor_removeHop => '去除啤酒花';
@override
String get pathEditor_unknownHop => '未知的重复器';
@override
String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。';
@@ -1934,54 +2026,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get common_clear => '清除';
@override
String path_currentPath(String path) {
return '当前路径:$path';
}
@override
String path_usingHopsPath(int count) {
return '使用 $count 跳路径';
}
@override
String get path_enterCustomPath => '输入自定义路径';
@override
String get path_currentPathLabel => '当前路径';
@override
String get path_hexPrefixInstructions => '请输入每个中继节点的2字符十六进制前缀,用逗号分隔。';
@override
String get path_hexPrefixExample => '例如:A1, F2, 3C(每个节点使用其公钥的第一字节)';
@override
String get path_labelHexPrefixes => '路径(十六进制前缀)';
@override
String get path_helperMaxHops => '最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。';
@override
String get path_selectFromContacts => '或从联系人列表中选择:';
@override
String get path_noRepeatersFound => '未找到任何转发节点或房间服务器。';
@override
String get path_customPathsRequire => '自定义路径需要中间节点转发消息。';
@override
String path_invalidHexPrefixes(String prefixes) {
return '无效的十六进制前缀:$prefixes';
}
@override
String get path_tooLong => '路径过长,最多允许 64 跳。';
@override
String get path_setPath => '设置路径';
@override
String get repeater_management => '转发节点管理';
@@ -2042,15 +2092,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_routingMode => '路由模式';
@override
String get repeater_autoUseSavedPath => '自动(使用保存的路径)';
@override
String get repeater_forceFloodMode => '强制泛洪模式';
@override
String get repeater_pathManagement => '路径管理';
@override
String get repeater_refresh => '刷新';
@@ -3011,6 +3052,139 @@ class AppLocalizationsZh extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_digitalInputLabel => '数字输入';
@override
String get telemetry_digitalOutputLabel => '数字输出';
@override
String get telemetry_analogInputLabel => '模拟输入';
@override
String get telemetry_analogOutputLabel => '模拟输出';
@override
String get telemetry_genericLabel => '通用传感器';
@override
String get telemetry_luminosityLabel => '照度';
@override
String get telemetry_presenceLabel => '存在检测';
@override
String get telemetry_humidityLabel => '湿度';
@override
String get telemetry_accelerometerLabel => '加速度计';
@override
String get telemetry_pressureLabel => '气压';
@override
String get telemetry_altitudeLabel => '高度';
@override
String get telemetry_frequencyLabel => '频率';
@override
String get telemetry_percentageLabel => '百分比';
@override
String get telemetry_concentrationLabel => '浓度';
@override
String get telemetry_powerLabel => '功率';
@override
String get telemetry_distanceLabel => '距离';
@override
String get telemetry_energyLabel => '能量';
@override
String get telemetry_directionLabel => '方向';
@override
String get telemetry_timeLabel => '时间';
@override
String get telemetry_gyrometerLabel => '陀螺仪';
@override
String get telemetry_colourLabel => '颜色';
@override
String get telemetry_gpsLabel => 'GPS';
@override
String get telemetry_switchLabel => '开关';
@override
String get telemetry_polylineLabel => '折线';
@override
String telemetry_altitudeValue(String meters) {
return '$meters m';
}
@override
String telemetry_frequencyValue(String hertz) {
return '$hertz Hz';
}
@override
String telemetry_pressureValue(String hpa) {
return '$hpa hPa';
}
@override
String telemetry_luminosityValue(String lux) {
return '$lux lx';
}
@override
String telemetry_powerValue(String watts) {
return '$watts W';
}
@override
String telemetry_distanceValue(String meters) {
return '$meters m';
}
@override
String telemetry_energyValue(String kilowattHours) {
return '$kilowattHours kWh';
}
@override
String telemetry_directionValue(String degrees) {
return '$degrees°';
}
@override
String telemetry_concentrationValue(String ppm) {
return '$ppm ppm';
}
@override
String telemetry_percentageValue(String percent) {
return '$percent%';
}
@override
String telemetry_analogValue(String value) {
return '$value';
}
@override
String get telemetry_autoFetchQuantity => '请求次数';
@override
String get telemetry_error => '无法获取数据';
@override
String get neighbors_receivedData => '已接收邻居信息';
@@ -3959,4 +4133,28 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contact_typeUnknown => 'Unknown';
@override
String get map_zoomIn => '放大';
@override
String get map_zoomOut => '放大';
@override
String get map_centerMap => '中心地图';
@override
String get chrome_bluetoothRequiresChromium =>
'Web Bluetooth 需要 Chromium 浏览器';
@override
String channels_communityShortId(String id) {
return 'ID$id...';
}
@override
String get pathTrace_legendGpsConfirmed => '通过GPS确认';
@override
String get pathTrace_legendInferred => '推测的位置';
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Verwijderen",
"common_enable": "Activeren",
"common_disable": "Uitschakelen",
"common_autoRefresh": "Automatisch vernieuwen",
"common_interval": "Tijdsinterval",
"common_reboot": "Herstarten",
"common_loading": "Laden...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Digitale ingang",
"telemetry_digitalOutputLabel": "Digitale uitgang",
"telemetry_analogInputLabel": "Analoge ingang",
"telemetry_analogOutputLabel": "Analoge uitgang",
"telemetry_genericLabel": "Algemene sensor",
"telemetry_luminosityLabel": "Lichtsterkte",
"telemetry_presenceLabel": "Aanwezigheid",
"telemetry_humidityLabel": "Luchtvochtigheid",
"telemetry_accelerometerLabel": "Versnellingsmeter",
"telemetry_pressureLabel": "Druk",
"telemetry_altitudeLabel": "Hoogte",
"telemetry_frequencyLabel": "Frequentie",
"telemetry_percentageLabel": "Percentage",
"telemetry_concentrationLabel": "Concentratie",
"telemetry_powerLabel": "Vermogen",
"telemetry_distanceLabel": "Afstand",
"telemetry_energyLabel": "Energie",
"telemetry_directionLabel": "Richting",
"telemetry_timeLabel": "Tijd",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Kleur",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Schakelaar",
"telemetry_polylineLabel": "Polylijn",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Aantal aanvragen",
"telemetry_error": "Kan gegevens niet ophalen",
"telemetry_noData": "Geen telemetriedata beschikbaar.",
"telemetry_channelTitle": "Kanaal {channel}",
"@telemetry_channelTitle": {
@@ -2314,5 +2353,98 @@
"chat_newMessages": "Nieuwe berichten",
"chat_markAsUnread": "Markeer als ongelezen",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data",
"repeater_chanUtil": "Gebruik van het kanaal"
"repeater_chanUtil": "Gebruik van het kanaal",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Verzonden",
"common_undo": "Achterhalen/Annuleren",
"messageStatus_delivered": "Leverd",
"messageStatus_pending": "Verzenden",
"messageStatus_failed": "Niet verzonden",
"messageStatus_repeated": "Hearsay, herhaald",
"contacts_moreOptions": "Meer opties",
"contacts_searchOpen": "Zoek contactpersonen",
"contacts_searchClose": "Zoeken",
"routing_title": "Routeplanning",
"routing_modeAuto": "Auto",
"routing_modeFlood": "Overstroming",
"routing_modeManual": "Handleiding",
"routing_modeAutoHint": "Selecteert automatisch het bekendste pad, en gebruikt een flood-algoritme als er geen bekend pad is.",
"routing_modeFloodHint": "Uitzendingen via elke zender. De meest betrouwbare methode, maar vereist meer uitzendtijd.",
"routing_modeManualHint": "Stuurt altijd de exacte route die u heeft aangegeven.",
"routing_currentRoute": "Huidige route",
"routing_directNoHops": "Direct zonder tussenliggende schakels",
"routing_noPathYet": "Er is nog geen route gevonden. De berichten blijven binnenkomen totdat een route is ontdekt.",
"routing_floodBroadcast": "Uitgestoten via elke zender.",
"routing_editPath": "Pad bewerken",
"routing_forgetPath": "Vergeet het pad",
"routing_knownPaths": "Bekende routes",
"routing_knownPathsHint": "Maak een route om er naartoe te gaan.",
"routing_inUse": "In gebruik",
"routing_qualityStrong": "Sterke eerste sprong",
"routing_qualityGood": "Een goede eerste stap",
"routing_qualityFair": "Een goede eerste hop",
"routing_qualityWorked": "Is geleverd",
"routing_qualityFlood": "Hears via een overstroming",
"routing_qualityUntested": "Niet getest",
"routing_neverWorked": "nooit bevestigd",
"routing_deliveryCounts": "{successes} zijn behaald, {failures} zijn mislukt",
"routing_floodDelivery": "Levering bij overstroming",
"pathEditor_title": "Pad creëren",
"pathEditor_hopCounter": "{count} van 64 hopgranen",
"pathEditor_noHops": "Er zijn nog geen hop toegevoegd. Klik op de onderstaande knoppen om ze in de juiste volgorde toe te voegen, of sla de bestelling op zonder hop om deze direct te versturen.",
"pathEditor_addHops": "Voeg hop toe in de juiste volgorde.",
"pathEditor_searchRepeaters": "Zoek naar herhaaldelijke zenders",
"pathEditor_advancedHex": "Geavanceerd: ruwe hex-pad",
"pathEditor_hexLabel": "Hex-voorkanten",
"pathEditor_hexHelper": "Twee hex-tekens per stap, gescheiden door komma's",
"pathEditor_invalidTokens": "Ongeldig: {tokens}",
"pathEditor_tooManyHops": "Maximaal 64 hopken",
"pathEditor_usePath": "Gebruik deze route.",
"pathEditor_removeHop": "Verwijder de hop",
"pathEditor_unknownHop": "Onbekend type zender",
"map_zoomIn": "Inzoomen",
"routing_lastWorked": "worked {when}",
"map_zoomOut": "Inzoomen",
"map_centerMap": "Centraal overzicht",
"chrome_bluetoothRequiresChromium": "Web Bluetooth vereist een Chromium-browser.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS-locatie bevestigd",
"pathTrace_legendInferred": "Afgeleide positie"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Usuń",
"common_enable": "Włącz",
"common_disable": "Wyłącz",
"common_autoRefresh": "Automatyczne odświeżanie",
"common_interval": "Interwał",
"common_reboot": "Uruchom ponownie",
"common_loading": "Ładowanie...",
"common_notAvailable": "—",
@@ -1290,6 +1292,43 @@
}
}
},
"telemetry_digitalInputLabel": "Wejście cyfrowe",
"telemetry_digitalOutputLabel": "Wyjście cyfrowe",
"telemetry_analogInputLabel": "Wejście analogowe",
"telemetry_analogOutputLabel": "Wyjście analogowe",
"telemetry_genericLabel": "Czujnik ogólny",
"telemetry_luminosityLabel": "Jasność",
"telemetry_presenceLabel": "Obecność",
"telemetry_humidityLabel": "Wilgotność",
"telemetry_accelerometerLabel": "Akcelerometr",
"telemetry_pressureLabel": "Ciśnienie",
"telemetry_altitudeLabel": "Wysokość",
"telemetry_frequencyLabel": "Częstotliwość",
"telemetry_percentageLabel": "Procent",
"telemetry_concentrationLabel": "Stężenie",
"telemetry_powerLabel": "Moc",
"telemetry_distanceLabel": "Odległość",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Kierunek",
"telemetry_timeLabel": "Czas",
"telemetry_gyrometerLabel": "Żyrometr",
"telemetry_colourLabel": "Kolor",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Przełącznik",
"telemetry_polylineLabel": "Polilinia",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Liczba żądań",
"telemetry_error": "Nie udało się pobrać danych",
"telemetry_noData": "Brak dostępnych danych telemetrycznych.",
"telemetry_channelTitle": "Kanał {channel}",
"@telemetry_channelTitle": {
@@ -2352,5 +2391,98 @@
"settings_companionDebugLogSubtitle": "Polecenia, odpowiedzi i surowe dane związane z protokołami BLE/TCP/USB",
"chat_markAsUnread": "Oznacz jako nieprzeczytane",
"settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)",
"repeater_chanUtil": "Wykorzystanie kanału"
"repeater_chanUtil": "Wykorzystanie kanału",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Wysłane",
"messageStatus_delivered": "Dostarczone",
"messageStatus_pending": "Wysyłanie",
"common_undo": "Wycofaj",
"messageStatus_failed": "Nie udało się wysłać",
"messageStatus_repeated": "Usłyszałem to wielokrotnie",
"contacts_moreOptions": "Więcej opcji",
"contacts_searchOpen": "Wyszukaj kontakty",
"contacts_searchClose": "Zaawansowane wyszukiwanie",
"routing_title": "Planowanie tras",
"routing_modeAuto": "Samochód",
"routing_modeFlood": "Powódź",
"routing_modeManual": "Instrukcja obsługi",
"routing_modeAutoHint": "Automatycznie wybiera najpopularniejszą ścieżkę, a w przypadku braku znanej, przechodzi do trybu \"przepływu\".",
"routing_modeFloodHint": "Transmisje za pośrednictwem każdego repeatera. Najbardziej niezawodna metoda, ale zużywa więcej czasu transmisji.",
"routing_modeManualHint": "Zawsze prowadzi dokładnie po trasie, którą określiłeś.",
"routing_currentRoute": "Obecna trasa",
"routing_directNoHops": "Bezpośrednio bez pośrednictwa repeaterów",
"routing_noPathYet": "Na razie nie ma żadnej ścieżki. Komunikacja trwa do momentu, gdy zostanie odkryta trasa.",
"routing_floodBroadcast": "Transmisja za pośrednictwem każdego urządzenia powielającego",
"routing_editPath": "Edytuj ścieżkę",
"routing_forgetPath": "Zapomnij o ścieżce",
"routing_knownPaths": "Znane trasy",
"routing_knownPathsHint": "Wybierz ścieżkę, aby przełączyć się na nią.",
"routing_inUse": "W użyciu",
"routing_qualityStrong": "Silny pierwszy skok",
"routing_qualityGood": "Świetny początek",
"routing_qualityFair": "Świetny pierwszy krzak",
"routing_qualityWorked": "Zostało dostarczone",
"routing_qualityFlood": "Usłyszano dzięki doniesieniom",
"routing_qualityUntested": "Nieużywany",
"routing_lastWorked": "pracował {when}",
"routing_neverWorked": "nigdy nie zostało potwierdzone",
"routing_floodDelivery": "Dostawa w przypadku powodzi",
"pathEditor_title": "Stworzenie ścieżki",
"pathEditor_hopCounter": "{count} z 64 rodzajów chmielu",
"pathEditor_noHops": "Na razie nie dodano żadnych chmielu. Aby dodać je w odpowiedniej kolejności, kliknij w odpowiednie przyciski poniżej, lub zapisz przepis bez chmielu, aby wysłać go bezpośrednio.",
"pathEditor_addHops": "Dodawaj chmiel zgodnie z kolejnością.",
"pathEditor_searchRepeaters": "Funkcje powtarzania",
"pathEditor_advancedHex": "Zaawansowane: ścieżka w formacie szesnastkowym",
"pathEditor_hexLabel": "Prefiksy heksadecymalne",
"pathEditor_hexHelper": "Dwa znaki szesnastkowe na każdym kroku, oddzielone przecinkami",
"pathEditor_invalidTokens": "Nieprawidłowe: {tokens}",
"pathEditor_tooManyHops": "Maksymalnie 64 hopów",
"pathEditor_usePath": "Użyj tej ścieżki.",
"pathEditor_removeHop": "Usuń dziką psiankę",
"pathEditor_unknownHop": "Nieznany repeater",
"map_zoomIn": "Przybliż",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Przybliż z powrotem",
"map_centerMap": "Mapa centrum",
"chrome_bluetoothRequiresChromium": "Web Bluetooth wymaga przeglądarki Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS potwierdzone",
"pathTrace_legendInferred": "Wywnioskowana pozycja"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Remover",
"common_enable": "Ativar",
"common_disable": "Desativar",
"common_autoRefresh": "Atualização automática",
"common_interval": "Intervalo",
"common_reboot": "Reiniciar",
"common_loading": "Carregando...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Entrada digital",
"telemetry_digitalOutputLabel": "Saída digital",
"telemetry_analogInputLabel": "Entrada analógica",
"telemetry_analogOutputLabel": "Saída analógica",
"telemetry_genericLabel": "Sensor genérico",
"telemetry_luminosityLabel": "Luminosidade",
"telemetry_presenceLabel": "Presença",
"telemetry_humidityLabel": "Humidade",
"telemetry_accelerometerLabel": "Acelerómetro",
"telemetry_pressureLabel": "Pressão",
"telemetry_altitudeLabel": "Altitude",
"telemetry_frequencyLabel": "Frequência",
"telemetry_percentageLabel": "Percentagem",
"telemetry_concentrationLabel": "Concentração",
"telemetry_powerLabel": "Potência",
"telemetry_distanceLabel": "Distância",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Direção",
"telemetry_timeLabel": "Hora",
"telemetry_gyrometerLabel": "Girómetro",
"telemetry_colourLabel": "Cor",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Interruptor",
"telemetry_polylineLabel": "Polilinha",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Número de solicitações",
"telemetry_error": "Não foi possível obter os dados",
"telemetry_noData": "Não estão disponíveis dados de telemetria.",
"telemetry_channelTitle": "Canal {channel}",
"@telemetry_channelTitle": {
@@ -2314,5 +2353,98 @@
"settings_companionDebugLogSubtitle": "Comandos, respostas e dados brutos para protocolos BLE/TCP/USB",
"chat_markAsUnread": "Marcar como não lido",
"chat_newMessages": "Novas mensagens",
"repeater_chanUtil": "Utilização do canal"
"repeater_chanUtil": "Utilização do canal",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"common_undo": "Desfazer",
"messageStatus_sent": "Enviado",
"messageStatus_pending": "Enviar",
"messageStatus_delivered": "Entregue",
"messageStatus_failed": "Falhou ao enviar",
"messageStatus_repeated": "Ouvi repetidamente",
"contacts_moreOptions": "Mais opções",
"contacts_searchOpen": "Pesquisar contatos",
"contacts_searchClose": "Pesquisa avançada",
"routing_title": "Rotas",
"routing_modeAuto": "Carro",
"routing_modeFlood": "Inundação",
"routing_modeManual": "Manual",
"routing_modeAutoHint": "Seleciona automaticamente o caminho mais conhecido, e, se nenhum caminho conhecido for encontrado, utiliza a estratégia de \"inundação\".",
"routing_modeFloodHint": "Transmissão através de todos os repetidores. É a opção mais confiável, mas utiliza mais tempo de transmissão.",
"routing_modeManualHint": "Sempre segue exatamente o caminho que você define.",
"routing_currentRoute": "Rota atual",
"routing_directNoHops": "Direto sem saltos de repetidor",
"routing_noPathYet": "Ainda não há um caminho definido. A mensagem continua a ser enviada até que uma rota seja encontrada.",
"routing_floodBroadcast": "Transmissão através de todos os repetidores",
"routing_editPath": "Editar caminho",
"routing_forgetPath": "Esqueça o caminho",
"routing_knownPaths": "Rotas conhecidas",
"routing_knownPathsHint": "Toque em um caminho para alternar para ele.",
"routing_inUse": "Em uso",
"routing_qualityStrong": "Primeiro salto notável",
"routing_qualityGood": "Primeiro salto bem-sucedido",
"routing_qualityFair": "Primeira etapa bem-sucedida",
"routing_qualityWorked": "Foi entregue",
"routing_qualityFlood": "Informação obtida através de relatos generalizados.",
"routing_qualityUntested": "Não testado",
"routing_neverWorked": "nunca confirmado",
"routing_floodDelivery": "Entrega em áreas afetadas por inundações",
"pathEditor_title": "Criar Caminho",
"pathEditor_hopCounter": "{count} de 64 gramas de lúpulo",
"pathEditor_noHops": "Ainda não há lúpulos adicionados. Clique nos repetidores abaixo para adicioná-los na ordem desejada, ou salve sem adicionar lúpulos para enviar diretamente.",
"pathEditor_addHops": "Adicione os lúpulos na seguinte ordem.",
"pathEditor_searchRepeaters": "Encontrar repetidores",
"pathEditor_advancedHex": "Avançado: caminho hexadecimal bruto",
"pathEditor_hexLabel": "Prefixos hexadecimais",
"pathEditor_hexHelper": "Dois caracteres hexadecimais por salto, separados por vírgulas.",
"pathEditor_invalidTokens": "Inválido: {tokens}",
"routing_lastWorked": "worked {when}",
"pathEditor_tooManyHops": "Máximo de 64 saltos",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_usePath": "Utilize este caminho.",
"pathEditor_removeHop": "Remova o lúpulo",
"pathEditor_unknownHop": "Repetidor desconhecido",
"map_zoomIn": "Ampliar",
"map_zoomOut": "Ampliar",
"map_centerMap": "Mapa do centro",
"chrome_bluetoothRequiresChromium": "O Web Bluetooth requer um navegador Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS confirmado",
"pathTrace_legendInferred": "Posição inferida"
}
+133 -1
View File
@@ -39,6 +39,8 @@
"common_notAvailable": "—",
"common_voltageValue": "{volts} В",
"common_percentValue": "{percent}%",
"common_autoRefresh": "Автообновление",
"common_interval": "Интервал",
"scanner_title": "MeshCore Open",
"scanner_scanning": "Поиск устройств...",
"scanner_connecting": "Подключение...",
@@ -686,6 +688,43 @@
"telemetry_voltageValue": "{volts}В",
"telemetry_currentValue": "{amps}А",
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"telemetry_digitalInputLabel": "Цифровой вход",
"telemetry_digitalOutputLabel": "Цифровой выход",
"telemetry_analogInputLabel": "Аналоговый вход",
"telemetry_analogOutputLabel": "Аналоговый выход",
"telemetry_genericLabel": "Общий датчик",
"telemetry_luminosityLabel": "Освещённость",
"telemetry_presenceLabel": "Присутствие",
"telemetry_humidityLabel": "Влажность",
"telemetry_accelerometerLabel": "Акселерометр",
"telemetry_pressureLabel": "Давление",
"telemetry_altitudeLabel": "Высота",
"telemetry_frequencyLabel": "Частота",
"telemetry_percentageLabel": "Процент",
"telemetry_concentrationLabel": "Концентрация",
"telemetry_powerLabel": "Мощность",
"telemetry_distanceLabel": "Расстояние",
"telemetry_energyLabel": "Энергия",
"telemetry_directionLabel": "Направление",
"telemetry_timeLabel": "Время",
"telemetry_gyrometerLabel": "Гирометр",
"telemetry_colourLabel": "Цвет",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Переключатель",
"telemetry_polylineLabel": "Полилиния",
"telemetry_altitudeValue": "{meters} м",
"telemetry_frequencyValue": "{hertz} Гц",
"telemetry_pressureValue": "{hpa} гПа",
"telemetry_luminosityValue": "{lux} лк",
"telemetry_powerValue": "{watts} Вт",
"telemetry_distanceValue": "{meters} м",
"telemetry_energyValue": "{kilowattHours} кВт⋅ч",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Количество запросов",
"telemetry_error": "Не удалось получить данные",
"neighbors_receivedData": "Полученные данные о соседях",
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
@@ -1617,5 +1656,98 @@
"repeater_cliHelpStatsCore": "(Только для серийного оборудования) Отображает основные статистические данные прошивки.",
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
"repeater_chanUtil": "Использование канала",
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)"
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_pending": "Отправка",
"common_undo": "Отменить",
"messageStatus_delivered": "Доставлено",
"messageStatus_sent": "Отправлено",
"messageStatus_failed": "Не удалось отправить",
"messageStatus_repeated": "Услышал несколько раз",
"contacts_moreOptions": "Больше вариантов",
"contacts_searchOpen": "Найти контакты",
"contacts_searchClose": "Закрыть поиск",
"routing_title": "Маршрутизация",
"routing_modeAuto": "Авто",
"routing_modeFlood": "Наводнение",
"routing_modeManual": "Инструкция",
"routing_modeAutoHint": "Автоматически выбирает наиболее известный путь, и если такой путь неизвестен, использует алгоритм поиска пути.",
"routing_modeFloodHint": "Передача сигнала через все ретрансляторы. Самый надежный способ, но требует больше времени на передачу.",
"routing_modeManualHint": "Всегда следует точно по указанному вами маршруту.",
"routing_currentRoute": "Текущий маршрут",
"routing_directNoHops": "Прямое соединение – без использования ретрансляторов",
"routing_noPathYet": "Пока нет пути. Следующее сообщение будет отправлено до тех пор, пока не будет обнаружен маршрут.",
"routing_floodBroadcast": "Транслируется через все ретрансляторы",
"routing_editPath": "Изменить путь",
"routing_forgetPath": "Забудьте о маршруте",
"routing_knownPaths": "Известные маршруты",
"routing_knownPathsHint": "Создайте маршрут для переключения на этот пункт.",
"routing_inUse": "В эксплуатации",
"routing_qualityStrong": "Сильный первый скачок",
"routing_qualityGood": "Хорошее начало",
"routing_qualityFair": "Первый хороший урожай",
"routing_qualityWorked": "Осуществлено",
"routing_qualityFlood": "Узнал из новостей, распространяющихся в интернете.",
"routing_qualityUntested": "Непроверенный",
"routing_neverWorked": "никогда не было подтверждено",
"routing_floodDelivery": "Доставка при затоплении",
"pathEditor_title": "Создать маршрут",
"pathEditor_hopCounter": "{count} из 64 хмеля",
"pathEditor_noHops": "На данный момент хмель еще не добавлен. Чтобы добавить его, нажмите на соответствующие кнопки ниже в нужном порядке, или сохраните рецепт без хмеля, чтобы отправить его напрямую.",
"pathEditor_addHops": "Добавляйте хмель в соответствии с указанным порядком.",
"pathEditor_searchRepeaters": "Поиск повторителей",
"pathEditor_advancedHex": "Продвинутый уровень: прямой путь в шестнадцатеричном формате",
"pathEditor_hexLabel": "Префиксы шестнадцатеричной системы",
"pathEditor_hexHelper": "Два шестнадцатеричных символа на каждом шаге, разделенные запятыми.",
"pathEditor_invalidTokens": "Неверно: {tokens}",
"routing_lastWorked": "worked {when}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "Максимальное количество ингредиентов – 64",
"pathEditor_usePath": "Используйте этот путь",
"pathEditor_removeHop": "Удалить хмель",
"pathEditor_unknownHop": "Неизвестный ретранслятор",
"map_zoomIn": "Увеличить масштаб",
"map_zoomOut": "Увеличить масштаб",
"map_centerMap": "Карта центра",
"chrome_bluetoothRequiresChromium": "Для работы Web Bluetooth требуется браузер на основе Chromium.",
"channels_communityShortId": "Идентификатор: {id}...",
"pathTrace_legendGpsConfirmed": "GPS подтверждено",
"pathTrace_legendInferred": "Выведенная позиция"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Odstrániť",
"common_enable": "Povolit",
"common_disable": "Zakázať",
"common_autoRefresh": "Automatické obnovenie",
"common_interval": "Časový interval",
"common_reboot": "Restartovať",
"common_loading": "Načítavanie...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Digitálny vstup",
"telemetry_digitalOutputLabel": "Digitálny výstup",
"telemetry_analogInputLabel": "Analógový vstup",
"telemetry_analogOutputLabel": "Analógový výstup",
"telemetry_genericLabel": "Všeobecný senzor",
"telemetry_luminosityLabel": "Osvetlenie",
"telemetry_presenceLabel": "Prítomnosť",
"telemetry_humidityLabel": "Vlhkosť",
"telemetry_accelerometerLabel": "Akcelerometer",
"telemetry_pressureLabel": "Tlak",
"telemetry_altitudeLabel": "Nadmorská výška",
"telemetry_frequencyLabel": "Frekvencia",
"telemetry_percentageLabel": "Percento",
"telemetry_concentrationLabel": "Koncentrácia",
"telemetry_powerLabel": "Výkon",
"telemetry_distanceLabel": "Vzdialenosť",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Smer",
"telemetry_timeLabel": "Čas",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Farba",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Prepínač",
"telemetry_polylineLabel": "Lomená čiara",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Počet požiadaviek",
"telemetry_error": "Nepodarilo sa získať údaje",
"telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.",
"telemetry_channelTitle": "Kanál {channel}",
"@telemetry_channelTitle": {
@@ -2314,5 +2353,98 @@
"settings_companionDebugLogSubtitle": "Príkazy, odpovede a surové dáta pre protokoly BLE/TCP/USB",
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
"chat_newMessages": "Nové správy",
"repeater_chanUtil": "Využitie kanálu"
"repeater_chanUtil": "Využitie kanálu",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Odoslané",
"messageStatus_delivered": "Doručené",
"messageStatus_pending": "Odoslanie",
"common_undo": "Zrušiť",
"messageStatus_failed": "Neúspešné odeslanie",
"messageStatus_repeated": "Slyšal som to opakovane",
"contacts_moreOptions": "Ďalšie možnosti",
"contacts_searchOpen": "Vyhľadajte kontakty",
"contacts_searchClose": "Zavrieť vyhľadávanie",
"routing_title": "Navigácia",
"routing_modeAuto": "Auto",
"routing_modeFlood": "Povodňová vlna",
"routing_modeManual": "Ručná príručka",
"routing_modeAutoHint": "Automaticky vyberá najznámejší trasa, a ak žiadna nie je známa, použije náhodnú trasu.",
"routing_modeFloodHint": "Prenos prostredníctvom všetkých opakovačov. Najspoľahlivejší spôsob, ale vyžaduje viac času vysielania.",
"routing_modeManualHint": "Vždy dodáva presne podľa zadaného trasy.",
"routing_currentRoute": "Aktuálna trasa",
"routing_directNoHops": "Priamo bez prechodných trás",
"routing_noPathYet": "Zatiaľ neexistuje žiadna cesta. Nasledujúce správy budú pokračovať, kým sa nenájde trasa.",
"routing_floodBroadcast": "Prenos prostredníctvom každého opakovača",
"routing_editPath": "Upraviť trasu",
"routing_forgetPath": "Zabudnite na trasu",
"routing_knownPaths": "Známe cesty",
"routing_knownPathsHint": "Kliknite na cestu, aby ste sa k nej presunuli.",
"routing_inUse": "V prevádzke",
"routing_qualityStrong": "Silný prvý krok",
"routing_qualityGood": "Úspešný prvý krok",
"routing_qualityFair": "Prvá, spravodlivá fáza",
"routing_qualityWorked": "Dosiahnutý úspech",
"routing_qualityFlood": "Zistil som to z informácií, ktoré som získal v dôsledku povodňovej situácie.",
"routing_qualityUntested": "Neotestované",
"routing_neverWorked": "nikedy nebolo potvrdené",
"routing_floodDelivery": "Doručenie v prípade povodní",
"pathEditor_title": "Vytvorenie cesty",
"pathEditor_hopCounter": "{count} z 64 chmelových zŕš",
"pathEditor_noHops": "Zatiaľ žiadne chmel. Kliknite na opakované, aby ste ich pridali postupne, alebo uložte bez chmelu, aby ste ho mohli poslať priamo.",
"pathEditor_addHops": "Pridávajte chmel podľa zadaného poriadku.",
"pathEditor_searchRepeaters": "Hľadať opakované",
"pathEditor_advancedHex": "Pokročilé: pôvodná hexová cesta",
"pathEditor_hexLabel": "Prefiksy pre hexadecimálne čísla",
"pathEditor_hexHelper": "Dve hexové čísla na každý krok, oddelené čiarkami",
"routing_lastWorked": "worked {when}",
"pathEditor_invalidTokens": "Neplatné: {tokens}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "Maximálne 64 krokov",
"pathEditor_usePath": "Použite túto cestu",
"pathEditor_removeHop": "Odstráňte chmel",
"pathEditor_unknownHop": "Neznáme zariadenie na opakované vysielanie",
"map_zoomIn": "Zväčšiť",
"map_zoomOut": "Zmenť zamer zblízka",
"map_centerMap": "Mapa centra",
"chrome_bluetoothRequiresChromium": "Web Bluetooth vyžaduje prehliadač Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS potvrdilo",
"pathTrace_legendInferred": "Odvodená poloha"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Izbrisati",
"common_enable": "Omogoči",
"common_disable": "Izklopiti",
"common_autoRefresh": "Samodejno osveževanje",
"common_interval": "Časovni interval",
"common_reboot": "Ponoviti",
"common_loading": "Naložanje...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Digitalni vhod",
"telemetry_digitalOutputLabel": "Digitalni izhod",
"telemetry_analogInputLabel": "Analogni vhod",
"telemetry_analogOutputLabel": "Analogni izhod",
"telemetry_genericLabel": "Splošni senzor",
"telemetry_luminosityLabel": "Osvetljenost",
"telemetry_presenceLabel": "Prisotnost",
"telemetry_humidityLabel": "Vlažnost",
"telemetry_accelerometerLabel": "Merilnik pospeška",
"telemetry_pressureLabel": "Tlak",
"telemetry_altitudeLabel": "Nadmorska višina",
"telemetry_frequencyLabel": "Frekvenca",
"telemetry_percentageLabel": "Odstotek",
"telemetry_concentrationLabel": "Koncentracija",
"telemetry_powerLabel": "Moč",
"telemetry_distanceLabel": "Razdalja",
"telemetry_energyLabel": "Energija",
"telemetry_directionLabel": "Smer",
"telemetry_timeLabel": "Čas",
"telemetry_gyrometerLabel": "Žiroskop",
"telemetry_colourLabel": "Barva",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Stikalo",
"telemetry_polylineLabel": "Polilinija",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Število zahtev",
"telemetry_error": "Podatkov ni bilo mogoče pridobiti",
"telemetry_noData": "Niso na voljo podatki o telemetriji.",
"telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": {
@@ -2314,5 +2353,98 @@
"chat_markAsUnread": "Označiti kot neneobdelano",
"chat_newMessages": "Nove novice",
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
"repeater_chanUtil": "Uporaba kanala"
"repeater_chanUtil": "Uporaba kanala",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"common_undo": "Preobrn",
"messageStatus_delivered": "Dostavljeno",
"messageStatus_sent": "Pošljeno",
"messageStatus_pending": "Pošiljanje",
"messageStatus_failed": "Uspešno ni bilo mogo, da se sporočilo pošlje",
"messageStatus_repeated": "Slišal sem večkrat",
"contacts_moreOptions": "Več možnosti",
"contacts_searchOpen": "Iskanje kontaktov",
"contacts_searchClose": "Izklopi iskanje",
"routing_title": "Navigacija",
"routing_modeAuto": "Avto",
"routing_modeFlood": "Poplavo",
"routing_modeManual": "Navodilo",
"routing_modeAutoHint": "Samodejno izbere najbolj poznano pot, in sicer, ko ni na voljo nobena.",
"routing_modeFloodHint": "Prenosi preko vseh repetitorjev. Najzanesljivejši način, vendar zahteva več časa.",
"routing_modeManualHint": "Vedno sledi natančni poti, ki jo ste določili.",
"routing_currentRoute": "Trenutna pot",
"routing_directNoHops": "Neposredno brez prehodov",
"routing_noPathYet": "Žep trenutno ni mogoče najti. Naslednje sporočilo bo posredovano, dokler ne bo ugotovljeno, kje je pot.",
"routing_floodBroadcast": "Prenos preko vseh repetitiv",
"routing_editPath": "Uredi pot",
"routing_forgetPath": "Pozabi na pot",
"routing_knownPaths": "Poznati poti",
"routing_knownPathsHint": "Kliknite na pot, da jo izberete.",
"routing_inUse": "V uporabi",
"routing_qualityStrong": "Močan prvi korak",
"routing_qualityGood": "Prva uspešna faza",
"routing_qualityFair": "Prva, uspešna faza",
"routing_qualityWorked": "Izpolnil",
"routing_qualityFlood": "Slišano preko poplave",
"routing_qualityUntested": "Ne preizkušen",
"routing_lastWorked": "delal/a {when}",
"routing_neverWorked": "nikoli ni bilo potrjeno",
"routing_floodDelivery": "Dostava zaradi poplave",
"pathEditor_title": "Izgradnja poti",
"pathEditor_hopCounter": "{count} od 64 različnih sort hropa",
"pathEditor_noHops": "Še niso dodani hmelji. Za dodajanje hmelja v vrstnem redu kliknite na povezavo spodaj, ali pa shranite brez dodanega hmelja, da ga lahko posredujete neposredno.",
"pathEditor_addHops": "Dodajte suho travo v skladu s postopkom.",
"pathEditor_searchRepeaters": "Iskanje ponovitev",
"pathEditor_advancedHex": "Napredno: surovi šestnajstni pot",
"pathEditor_hexLabel": "Predfiks za heksadecimalno šifro",
"pathEditor_hexHelper": "Dva šestbitna znaka na vsak skok, ločena z vejico",
"pathEditor_invalidTokens": "Neveljaven: {tokens}",
"pathEditor_tooManyHops": "Največ 64 hopov",
"pathEditor_usePath": "Uporabite to poto",
"pathEditor_removeHop": "Odstranite hmelj",
"pathEditor_unknownHop": "Neznani ponovitelj",
"map_zoomIn": "Povečaj",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Povečajte pogled",
"map_centerMap": "Krajšarska karta",
"chrome_bluetoothRequiresChromium": "Web Bluetooth zahteva brskalnik Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS potrdilo",
"pathTrace_legendInferred": "Izpeljana lokacija"
}
+133 -1
View File
@@ -33,6 +33,8 @@
"common_remove": "Ta bort",
"common_enable": "Aktivera",
"common_disable": "Inaktivera",
"common_autoRefresh": "Automatisk uppdatering",
"common_interval": "Intervall",
"common_reboot": "Start om",
"common_loading": "Laddar...",
"common_notAvailable": "—",
@@ -1280,6 +1282,43 @@
}
}
},
"telemetry_digitalInputLabel": "Digital ingång",
"telemetry_digitalOutputLabel": "Digital utgång",
"telemetry_analogInputLabel": "Analog ingång",
"telemetry_analogOutputLabel": "Analog utgång",
"telemetry_genericLabel": "Allmän sensor",
"telemetry_luminosityLabel": "Ljusstyrka",
"telemetry_presenceLabel": "Närvaro",
"telemetry_humidityLabel": "Luftfuktighet",
"telemetry_accelerometerLabel": "Accelerometer",
"telemetry_pressureLabel": "Tryck",
"telemetry_altitudeLabel": "Höjd",
"telemetry_frequencyLabel": "Frekvens",
"telemetry_percentageLabel": "Procent",
"telemetry_concentrationLabel": "Koncentration",
"telemetry_powerLabel": "Effekt",
"telemetry_distanceLabel": "Avstånd",
"telemetry_energyLabel": "Energi",
"telemetry_directionLabel": "Riktning",
"telemetry_timeLabel": "Tid",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Färg",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Brytare",
"telemetry_polylineLabel": "Polylinje",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Antal förfrågningar",
"telemetry_error": "Det gick inte att hämta data",
"telemetry_noData": "Inga telemetridata tillgängliga.",
"telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": {
@@ -2314,5 +2353,98 @@
"settings_companionDebugLog": "Följande felsökningslogg",
"chat_newMessages": "Nya meddelanden",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
"repeater_chanUtil": "Användning av kanal"
"repeater_chanUtil": "Användning av kanal",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Sen",
"messageStatus_delivered": "Levererad",
"common_undo": "Ångra",
"messageStatus_pending": "Skicka",
"messageStatus_failed": "Misslyckades med att skicka",
"messageStatus_repeated": "Hördes upprepade gånger",
"contacts_moreOptions": "Fler alternativ",
"contacts_searchOpen": "Sök efter kontakter",
"contacts_searchClose": "Avancerad sökning",
"routing_title": "Ruttplanering",
"routing_modeAuto": "Bil",
"routing_modeFlood": "Översvämning",
"routing_modeManual": "Instruktioner",
"routing_modeAutoHint": "Väljer automatiskt den bästa kända vägen, och använder en \"flooding\"-strategi om ingen väg är känd.",
"routing_modeFloodHint": "Sändningar via alla repetrar. Det mest pålitliga alternativet, men kräver mer sändtid.",
"routing_modeManualHint": "Skickar alltid den exakta väg du har angivit.",
"routing_currentRoute": "Nuvarande rutt",
"routing_directNoHops": "Direkt utan mellanliggande routrar",
"routing_noPathYet": "Ingen väg hittad ännu. Nästa meddelande skickas tills en rutt har upptäckts.",
"routing_floodBroadcast": "Sändas via alla repetrar",
"routing_editPath": "Redigera sökväg",
"routing_forgetPath": "Glöm vägen",
"routing_knownPaths": "Kända vägar",
"routing_knownPathsHint": "Välj en väg för att byta till den.",
"routing_inUse": "I användning",
"routing_qualityStrong": "En stark start",
"routing_qualityGood": "Bra första steg",
"routing_qualityFair": "Bra första hopp",
"routing_qualityWorked": "Har levererat",
"routing_qualityFlood": "Fått information via nyhetsflöde",
"routing_qualityUntested": "Ej testat",
"routing_lastWorked": "arbetade {when}",
"routing_neverWorked": "aldrig bekräftat",
"routing_floodDelivery": "Leverans vid översvämningsområde",
"pathEditor_title": "Skapa väg",
"pathEditor_hopCounter": "{count} av 64 humlor",
"pathEditor_noHops": "Inga humle än. Använd knapparna nedan för att lägga till dem i rätt ordning, eller spara utan humle för att skicka direkt.",
"pathEditor_addHops": "Tillsätt humlen i rätt ordning.",
"pathEditor_searchRepeaters": "Sök efter återupptagna samtal",
"pathEditor_advancedHex": "Avancerat: rå hex-sökväg",
"pathEditor_hexLabel": "Hex-prefikser",
"pathEditor_hexHelper": "Två hex-tecken per steg, separerade med kommatecken.",
"pathEditor_invalidTokens": "Ogiltigt: {tokens}",
"pathEditor_tooManyHops": "Maximalt 64 humlörter",
"pathEditor_usePath": "Använd denna väg",
"pathEditor_removeHop": "Ta bort humlen",
"pathEditor_unknownHop": "Okänd förstärkare",
"map_zoomIn": "Zooma in",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Zooma ut",
"map_centerMap": "Kartöversikt",
"chrome_bluetoothRequiresChromium": "Web Bluetooth kräver en Chromium-baserad webbläsare.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS-verifierat",
"pathTrace_legendInferred": "Antagen position"
}
+133 -1
View File
@@ -34,6 +34,8 @@
"common_remove": "Прибрати",
"common_enable": "Увімкнути",
"common_disable": "Вимкнути",
"common_autoRefresh": "Автооновлення",
"common_interval": "Інтервал",
"common_reboot": "Перезавантажити",
"common_loading": "Завантаження...",
"common_notAvailable": "—",
@@ -1291,6 +1293,43 @@
}
}
},
"telemetry_digitalInputLabel": "Цифровий вхід",
"telemetry_digitalOutputLabel": "Цифровий вихід",
"telemetry_analogInputLabel": "Аналоговий вхід",
"telemetry_analogOutputLabel": "Аналоговий вихід",
"telemetry_genericLabel": "Загальний датчик",
"telemetry_luminosityLabel": "Освітленість",
"telemetry_presenceLabel": "Присутність",
"telemetry_humidityLabel": "Вологість",
"telemetry_accelerometerLabel": "Акселерометр",
"telemetry_pressureLabel": "Тиск",
"telemetry_altitudeLabel": "Висота",
"telemetry_frequencyLabel": "Частота",
"telemetry_percentageLabel": "Відсоток",
"telemetry_concentrationLabel": "Концентрація",
"telemetry_powerLabel": "Потужність",
"telemetry_distanceLabel": "Відстань",
"telemetry_energyLabel": "Енергія",
"telemetry_directionLabel": "Напрямок",
"telemetry_timeLabel": "Час",
"telemetry_gyrometerLabel": "Гірометр",
"telemetry_colourLabel": "Колір",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Перемикач",
"telemetry_polylineLabel": "Полілінія",
"telemetry_altitudeValue": "{meters} м",
"telemetry_frequencyValue": "{hertz} Гц",
"telemetry_pressureValue": "{hpa} гПа",
"telemetry_luminosityValue": "{lux} лк",
"telemetry_powerValue": "{watts} Вт",
"telemetry_distanceValue": "{meters} м",
"telemetry_energyValue": "{kilowattHours} кВт⋅год",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Кількість запитів",
"telemetry_error": "Не вдалося отримати дані",
"telemetry_noData": "Дані телеметрії недоступні.",
"telemetry_channelTitle": "Канал {channel}",
"@telemetry_channelTitle": {
@@ -2294,5 +2333,98 @@
"settings_companionDebugLogSubtitle": "Команди, відповіді та необроблена інформація для протоколів BLE/TCP/USB",
"chat_newMessages": "Нові повідомлення",
"chat_markAsUnread": "Позначити як непрочитане",
"repeater_chanUtil": "Використання каналу"
"repeater_chanUtil": "Використання каналу",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_delivered": "Доставлено",
"messageStatus_sent": "Надіслано",
"common_undo": "Скасувати",
"messageStatus_pending": "Надсилання",
"messageStatus_failed": "Не вдалося надіслати",
"messageStatus_repeated": "Почув неодноразово",
"contacts_moreOptions": "Більше можливостей",
"contacts_searchOpen": "Пошук контактів",
"contacts_searchClose": "Закрити пошук",
"routing_title": "Маршрутизація",
"routing_modeAuto": "Автомобіль",
"routing_modeFlood": "Повені",
"routing_modeManual": "Інструкція",
"routing_modeAutoHint": "Автоматично обирає найкращий відомий шлях, та у разі відсутності відомого шляху, використовує алгоритм \"занурення\".",
"routing_modeFloodHint": "Передавання через усі ретранслятори. Найбільш надійний спосіб, але потребує більше часу.",
"routing_modeManualHint": "Завжди доставляє точно за вказаним вами маршрутом.",
"routing_currentRoute": "Поточний маршрут",
"routing_directNoHops": "Пряме з'єднання – без проміжних ретрансляторів",
"routing_noPathYet": "Поки що немає жодного шляху. Повідомлення продовжуються надходити, поки не буде знайдено маршрут.",
"routing_floodBroadcast": "Поширення через усі ретранслятори",
"routing_editPath": "Редагувати шлях",
"routing_forgetPath": "Забудь про шлях",
"routing_knownPaths": "Відомі маршрути",
"routing_knownPathsHint": "Виберіть опцію, щоб переключитися на неї.",
"routing_inUse": "У робочому стані",
"routing_qualityStrong": "Сильний перший стрибок",
"routing_qualityGood": "Чудова перша спроба",
"routing_qualityFair": "Перший, але вдалий, крок",
"routing_qualityWorked": "Доставлено",
"routing_qualityFlood": "Дізнався через новини",
"routing_qualityUntested": "Не протестовано",
"routing_neverWorked": "ніколи не підтверджено",
"routing_floodDelivery": "Доставка під час повені",
"pathEditor_title": "Створити маршрут",
"pathEditor_hopCounter": "{count} з 64 штук хмелю",
"pathEditor_noHops": "Ще не додано хміль. Натисніть на відповідні кнопки, щоб додати його в потрібному порядку, або збережіть рецепт без хмілю, щоб відправити його безпосередньо.",
"pathEditor_addHops": "Додавайте хміль у наступній послідовності.",
"pathEditor_searchRepeaters": "Пошук повторювачів",
"pathEditor_advancedHex": "Просунутий рівень: пряма шлях у форматі шестнадцяткової системи.",
"pathEditor_hexLabel": "Префікси для шестнадцяткової системи числення",
"pathEditor_hexHelper": "Два шестизначні символи на кожний крок, розділені комами",
"pathEditor_invalidTokens": "Неправильно: {tokens}",
"routing_lastWorked": "worked {when}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "Максимум 64 хмелеві колоди",
"pathEditor_usePath": "Використовуйте цей шлях",
"pathEditor_removeHop": "Видалити хміль",
"pathEditor_unknownHop": "Невідомий ретранслятор",
"map_zoomIn": "Увійти в режим збільшення",
"map_zoomOut": "Видалити зум",
"map_centerMap": "Карта центру",
"chrome_bluetoothRequiresChromium": "Web Bluetooth вимагає браузера на основі Chromium",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS підтверджено",
"pathTrace_legendInferred": "Висновок щодо положення"
}
+133 -1
View File
@@ -34,6 +34,8 @@
"common_remove": "移除",
"common_enable": "启用",
"common_disable": "禁用",
"common_autoRefresh": "自动刷新",
"common_interval": "间隔",
"common_reboot": "重启",
"common_loading": "正在加载...",
"common_notAvailable": "—",
@@ -1310,6 +1312,43 @@
}
}
},
"telemetry_digitalInputLabel": "数字输入",
"telemetry_digitalOutputLabel": "数字输出",
"telemetry_analogInputLabel": "模拟输入",
"telemetry_analogOutputLabel": "模拟输出",
"telemetry_genericLabel": "通用传感器",
"telemetry_luminosityLabel": "照度",
"telemetry_presenceLabel": "存在检测",
"telemetry_humidityLabel": "湿度",
"telemetry_accelerometerLabel": "加速度计",
"telemetry_pressureLabel": "气压",
"telemetry_altitudeLabel": "高度",
"telemetry_frequencyLabel": "频率",
"telemetry_percentageLabel": "百分比",
"telemetry_concentrationLabel": "浓度",
"telemetry_powerLabel": "功率",
"telemetry_distanceLabel": "距离",
"telemetry_energyLabel": "能量",
"telemetry_directionLabel": "方向",
"telemetry_timeLabel": "时间",
"telemetry_gyrometerLabel": "陀螺仪",
"telemetry_colourLabel": "颜色",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "开关",
"telemetry_polylineLabel": "折线",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "请求次数",
"telemetry_error": "无法获取数据",
"telemetry_noData": "暂无遥测数据",
"telemetry_channelTitle": "频道 {channel}",
"@telemetry_channelTitle": {
@@ -2319,5 +2358,98 @@
"settings_companionDebugLog": "调试日志",
"chat_newMessages": "新的消息",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
"repeater_chanUtil": "频道利用率"
"repeater_chanUtil": "频道利用率",
"@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "发送",
"common_undo": "撤销",
"messageStatus_delivered": "已送达",
"messageStatus_pending": "发送",
"messageStatus_failed": "发送失败",
"messageStatus_repeated": "多次听到",
"contacts_moreOptions": "更多选择",
"contacts_searchOpen": "搜索联系人",
"contacts_searchClose": "高级搜索",
"routing_title": "路由",
"routing_modeAuto": "汽车",
"routing_modeFlood": "洪水",
"routing_modeManual": "手册",
"routing_modeAutoHint": "自动选择已知最佳路径,当没有已知路径时,则进行“洪水”搜索。",
"routing_modeFloodHint": "通过所有中继站进行广播。 这种方式最可靠,但占用更多的时间。",
"routing_modeManualHint": "总是按照您设置的路径进行导航。",
"routing_currentRoute": "当前路线",
"routing_directNoHops": "直接连接— 无中继跳",
"routing_noPathYet": "目前还没有找到路径。直到找到路径,才会收到后续消息。",
"routing_floodBroadcast": "通过所有中继器进行广播",
"routing_editPath": "编辑路径",
"routing_forgetPath": "忘记原路",
"routing_knownPaths": "已知的路径",
"routing_knownPathsHint": "点击该路径以切换到它。",
"routing_inUse": "使用中",
"routing_qualityStrong": "强劲的初始阶段",
"routing_qualityGood": "不错的开端",
"routing_qualityFair": "第一次尝试,结果良好",
"routing_qualityWorked": "已完成",
"routing_qualityFlood": "通过新闻报道",
"routing_qualityUntested": "未经测试",
"routing_lastWorked": "工作于 {when}",
"routing_neverWorked": "从未得到证实",
"routing_floodDelivery": "洪水配送",
"pathEditor_title": "构建路径",
"pathEditor_noHops": "目前还没有添加任何啤酒花。点击下面的“添加”按钮,按顺序添加,或者直接保存,不添加任何啤酒花。",
"pathEditor_addHops": "按照顺序添加啤酒花",
"pathEditor_searchRepeaters": "重复搜索",
"pathEditor_advancedHex": "高级:原始十六进制路径",
"pathEditor_hexLabel": "十六进制前缀",
"pathEditor_hexHelper": "每次跳跃,使用两个十六进制字符,用逗号分隔。",
"pathEditor_invalidTokens": "无效:{tokens}",
"pathEditor_tooManyHops": "最多 64 个跳跃",
"pathEditor_usePath": "请使用此路径",
"pathEditor_removeHop": "去除啤酒花",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_hopCounter": "{count} of 64 hops",
"pathEditor_unknownHop": "未知的重复器",
"map_zoomIn": "放大",
"map_zoomOut": "放大",
"map_centerMap": "中心地图",
"chrome_bluetoothRequiresChromium": "Web Bluetooth 需要 Chromium 浏览器",
"channels_communityShortId": "ID{id}...",
"pathTrace_legendGpsConfirmed": "通过GPS确认",
"pathTrace_legendInferred": "推测的位置"
}
+12 -17
View File
@@ -24,11 +24,21 @@ import 'services/translation_service.dart';
import 'services/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart';
import 'theme/mesh_theme.dart';
import 'utils/app_logger.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// On desktop, debugPrint is not suppressed in release builds and every
// call is a synchronous stdout write. The connector logs heavily on hot
// paths (frame handling, queue/channel sync), which shows up as syscall
// overhead on low-end Linux machines (issue #202). The in-app debug log
// screens are unaffected they store entries themselves.
if (kReleaseMode) {
debugPrint = (String? message, {int? wrapWidth}) {};
}
// Initialize SharedPreferences cache
await PrefsManager.initialize();
@@ -191,23 +201,8 @@ class MeshCoreApp extends StatelessWidget {
locale: _localeFromSetting(
settingsService.settings.languageOverride,
),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
theme: MeshTheme.light(),
darkTheme: MeshTheme.dark(),
themeMode: _themeModeFromSetting(
settingsService.settings.themeMode,
),
+17 -10
View File
@@ -63,7 +63,7 @@ class AppDebugLogScreen extends StatelessWidget {
final entry = entries[index];
return ListTile(
dense: true,
leading: _buildLevelIcon(entry.level),
leading: _buildLevelIcon(context, entry.level),
title: Text(
'[${entry.tag}] ${entry.message}',
style: const TextStyle(
@@ -75,7 +75,9 @@ class AppDebugLogScreen extends StatelessWidget {
entry.formattedTime,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
);
@@ -88,14 +90,16 @@ class AppDebugLogScreen extends StatelessWidget {
Icon(
Icons.bug_report_outlined,
size: 64,
color: Colors.grey[400],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
context.l10n.debugLog_noEntries,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
@@ -103,7 +107,9 @@ class AppDebugLogScreen extends StatelessWidget {
context.l10n.debugLog_enableInSettings,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
@@ -115,18 +121,19 @@ class AppDebugLogScreen extends StatelessWidget {
);
}
Widget _buildLevelIcon(AppDebugLogLevel level) {
Widget _buildLevelIcon(BuildContext context, AppDebugLogLevel level) {
final colorScheme = Theme.of(context).colorScheme;
switch (level) {
case AppDebugLogLevel.info:
return const Icon(Icons.info_outline, size: 18, color: Colors.blue);
return Icon(Icons.info_outline, size: 18, color: colorScheme.primary);
case AppDebugLogLevel.warning:
return const Icon(
return Icon(
Icons.warning_amber_outlined,
size: 18,
color: Colors.orange,
color: colorScheme.tertiary,
);
case AppDebugLogLevel.error:
return const Icon(Icons.error_outline, size: 18, color: Colors.red);
return Icon(Icons.error_outline, size: 18, color: colorScheme.error);
}
}
}
+108 -101
View File
@@ -92,11 +92,29 @@ class AppSettingsScreen extends StatelessWidget {
ListTile(
leading: const Icon(Icons.brightness_6_outlined),
title: Text(context.l10n.appSettings_theme),
subtitle: Text(
_themeModeLabel(context, settingsService.settings.themeMode),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8),
child: SegmentedButton<String>(
segments: [
ButtonSegment(
value: 'system',
label: Text(context.l10n.appSettings_themeSystem),
),
ButtonSegment(
value: 'light',
label: Text(context.l10n.appSettings_themeLight),
),
ButtonSegment(
value: 'dark',
label: Text(context.l10n.appSettings_themeDark),
),
],
selected: {settingsService.settings.themeMode},
onSelectionChanged: (selection) {
settingsService.setThemeMode(selection.first);
},
),
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeModeDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
@@ -111,18 +129,6 @@ class AppSettingsScreen extends StatelessWidget {
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(context, settingsService),
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.location_searching),
title: Text(context.l10n.appSettings_enableMessageTracing),
subtitle: Text(
context.l10n.appSettings_enableMessageTracingSubtitle,
),
value: settingsService.settings.enableMessageTracing,
onChanged: (value) {
settingsService.setEnableMessageTracing(value);
},
),
],
),
);
@@ -189,14 +195,14 @@ class AppSettingsScreen extends StatelessWidget {
Icons.message_outlined,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
title: Text(
context.l10n.appSettings_messageNotifications,
style: TextStyle(
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
),
subtitle: Text(
@@ -204,7 +210,7 @@ class AppSettingsScreen extends StatelessWidget {
style: TextStyle(
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
),
value: settingsService.settings.notifyOnNewMessage,
@@ -220,14 +226,14 @@ class AppSettingsScreen extends StatelessWidget {
Icons.forum_outlined,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
title: Text(
context.l10n.appSettings_channelMessageNotifications,
style: TextStyle(
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
),
subtitle: Text(
@@ -235,7 +241,7 @@ class AppSettingsScreen extends StatelessWidget {
style: TextStyle(
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
),
value: settingsService.settings.notifyOnNewChannelMessage,
@@ -251,14 +257,14 @@ class AppSettingsScreen extends StatelessWidget {
Icons.cell_tower,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
title: Text(
context.l10n.appSettings_advertisementNotifications,
style: TextStyle(
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
),
subtitle: Text(
@@ -266,7 +272,7 @@ class AppSettingsScreen extends StatelessWidget {
style: TextStyle(
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
: Theme.of(context).disabledColor,
),
),
value: settingsService.settings.notifyOnNewAdvert,
@@ -343,7 +349,13 @@ class AppSettingsScreen extends StatelessWidget {
);
},
),
if (settingsService.settings.autoRouteRotationEnabled) ...[
if (settingsService.settings.autoRouteRotationEnabled)
Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
padding: const EdgeInsets.only(left: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_maxRouteWeight),
@@ -371,7 +383,9 @@ class AppSettingsScreen extends StatelessWidget {
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_initialRouteWeightSubtitle),
Text(
context.l10n.appSettings_initialRouteWeightSubtitle,
),
Slider(
value: settingsService.settings.initialRouteWeight,
min: 0.5,
@@ -387,7 +401,9 @@ class AppSettingsScreen extends StatelessWidget {
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_routeWeightSuccessIncrement),
title: Text(
context.l10n.appSettings_routeWeightSuccessIncrement,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -397,21 +413,27 @@ class AppSettingsScreen extends StatelessWidget {
.appSettings_routeWeightSuccessIncrementSubtitle,
),
Slider(
value: settingsService.settings.routeWeightSuccessIncrement,
value: settingsService
.settings
.routeWeightSuccessIncrement,
min: 0.1,
max: 2.0,
divisions: 19,
label: settingsService.settings.routeWeightSuccessIncrement
label: settingsService
.settings
.routeWeightSuccessIncrement
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setRouteWeightSuccessIncrement(value),
onChanged: (value) => settingsService
.setRouteWeightSuccessIncrement(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_routeWeightFailureDecrement),
title: Text(
context.l10n.appSettings_routeWeightFailureDecrement,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -421,14 +443,18 @@ class AppSettingsScreen extends StatelessWidget {
.appSettings_routeWeightFailureDecrementSubtitle,
),
Slider(
value: settingsService.settings.routeWeightFailureDecrement,
value: settingsService
.settings
.routeWeightFailureDecrement,
min: 0.1,
max: 2.0,
divisions: 19,
label: settingsService.settings.routeWeightFailureDecrement
label: settingsService
.settings
.routeWeightFailureDecrement
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setRouteWeightFailureDecrement(value),
onChanged: (value) => settingsService
.setRouteWeightFailureDecrement(value),
),
],
),
@@ -439,7 +465,9 @@ class AppSettingsScreen extends StatelessWidget {
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_maxMessageRetriesSubtitle),
Text(
context.l10n.appSettings_maxMessageRetriesSubtitle,
),
Slider(
value: settingsService.settings.maxMessageRetries
.toDouble(),
@@ -448,13 +476,27 @@ class AppSettingsScreen extends StatelessWidget {
divisions: 8,
label: settingsService.settings.maxMessageRetries
.toString(),
onChanged: (value) =>
settingsService.setMaxMessageRetries(value.toInt()),
onChanged: (value) => settingsService
.setMaxMessageRetries(value.toInt()),
),
],
),
),
],
),
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.location_searching),
title: Text(context.l10n.appSettings_enableMessageTracing),
subtitle: Text(
context.l10n.appSettings_enableMessageTracingSubtitle,
),
value: settingsService.settings.enableMessageTracing,
onChanged: (value) {
settingsService.setEnableMessageTracing(value);
},
),
],
),
);
@@ -584,15 +626,25 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile(
secondary: Icon(
Icons.auto_awesome_outlined,
color: translationEnabled ? null : Colors.grey,
color: translationEnabled
? null
: Theme.of(context).disabledColor,
),
title: Text(
context.l10n.translation_autoIncomingTitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey),
style: TextStyle(
color: translationEnabled
? null
: Theme.of(context).disabledColor,
),
),
subtitle: Text(
context.l10n.translation_autoIncomingSubtitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey),
style: TextStyle(
color: translationEnabled
? null
: Theme.of(context).disabledColor,
),
),
value: settings.autoTranslateIncomingMessages,
onChanged: translationEnabled
@@ -603,15 +655,25 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile(
secondary: Icon(
Icons.outgoing_mail,
color: translationEnabled ? null : Colors.grey,
color: translationEnabled
? null
: Theme.of(context).disabledColor,
),
title: Text(
context.l10n.translation_composerTitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey),
style: TextStyle(
color: translationEnabled
? null
: Theme.of(context).disabledColor,
),
),
subtitle: Text(
context.l10n.translation_composerSubtitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey),
style: TextStyle(
color: translationEnabled
? null
: Theme.of(context).disabledColor,
),
),
value: settings.composerTranslationEnabled,
onChanged: translationEnabled
@@ -871,61 +933,6 @@ class AppSettingsScreen extends StatelessWidget {
);
}
void _showThemeModeDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.appSettings_theme),
content: RadioGroup<String>(
groupValue: settingsService.settings.themeMode,
onChanged: (value) {
if (value != null) {
settingsService.setThemeMode(value);
Navigator.pop(context);
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<String>(
title: Text(context.l10n.appSettings_themeSystem),
value: 'system',
),
RadioListTile<String>(
title: Text(context.l10n.appSettings_themeLight),
value: 'light',
),
RadioListTile<String>(
title: Text(context.l10n.appSettings_themeDark),
value: 'dark',
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
),
);
}
String _themeModeLabel(BuildContext context, String value) {
switch (value) {
case 'light':
return context.l10n.appSettings_themeLight;
case 'dark':
return context.l10n.appSettings_themeDark;
default:
return context.l10n.appSettings_themeSystem;
}
}
String _languageLabel(BuildContext context, String? languageCode) {
switch (languageCode) {
case 'en':
+164 -184
View File
@@ -5,7 +5,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
@@ -26,8 +26,9 @@ import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/translation_service.dart';
import '../utils/emoji_utils.dart';
import '../helpers/contact_ui.dart';
import '../widgets/byte_count_input.dart';
import '../widgets/empty_state.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
@@ -284,6 +285,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
)
: widget.channel.name,
style: const TextStyle(fontSize: 16),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
@@ -312,9 +315,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'clearChat') {
context.read<MeshCoreConnector>().clearMessagesForChannel(
widget.channel.index,
);
_confirmClearChat();
}
},
itemBuilder: (context) => [
@@ -322,11 +323,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
value: 'clearChat',
child: Row(
children: [
const Icon(Icons.delete, size: 20, color: Colors.red),
Icon(
Icons.delete,
size: 20,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 12),
Text(
context.l10n.contact_clearChat,
style: const TextStyle(color: Colors.red),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
],
),
@@ -345,34 +352,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final messages = connector.getChannelMessages(widget.channel);
if (messages.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.channel.isPublicChannel
return EmptyState(
icon: widget.channel.isPublicChannel
? Icons.public
: Icons.tag,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
context.l10n.chat_noMessages,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
context.l10n.chat_sendMessageToStart,
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
title: context.l10n.chat_noMessages,
subtitle: context.l10n.chat_sendMessageTo(
widget.channel.name.isEmpty
? context.l10n.channels_channelIndex(
widget.channel.index,
)
: widget.channel.name,
),
);
}
@@ -382,6 +372,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final itemCount =
reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Prune stale keys (deleted/cleared messages) to avoid
// unbounded growth.
final liveIds = reversedMessages
.map((m) => m.messageId)
.toSet();
_messageKeys.removeWhere((id, _) => !liveIds.contains(id));
// Two messages can collide on messageId (same ms + name/text
// hash). Only the first occurrence owns the shared GlobalKey
// used for scroll-to-message; duplicates get a local key so
// no two widgets share one GlobalKey.
final seenIds = <String>{};
final keyedIndices = <int>{};
for (var i = 0; i < reversedMessages.length; i++) {
if (seenIds.add(reversedMessages[i].messageId)) {
keyedIndices.add(i);
}
}
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_channelSkipNextBottomSnap) {
@@ -417,14 +426,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
final GlobalKey messageKey;
if (keyedIndices.contains(messageIndex)) {
messageKey = _messageKeys.putIfAbsent(
message.messageId,
GlobalKey.new,
);
} else {
messageKey = GlobalKey();
}
final isUnreadAnchor =
_unreadDividerMessageId != null &&
message.messageId == _unreadDividerMessageId;
return Container(
key: _messageKeys[message.messageId]!,
key: messageKey,
child: Builder(
builder: (context) {
final textScale = context
@@ -499,7 +514,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const maxSwipeOffset = 64.0;
const replySwipeThreshold = 64.0;
const bodyFontSize = 14.0;
final messageBody = Column(
final messageBody = LayoutBuilder(
builder: (context, constraints) => Column(
crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
@@ -516,9 +532,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
Flexible(
child: GestureDetector(
onTap: PlatformInfo.isDesktop
? null
: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
onSecondaryTapUp: PlatformInfo.isDesktop
? (_) => _showMessageActions(message)
@@ -526,14 +539,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
maxWidth: constraints.maxWidth * 0.65,
),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
@@ -570,20 +588,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
isOutgoing,
textScale,
message.senderName,
trailing: (!enableTracing && isOutgoing)
? Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
)
: null,
)
else if (gifId != null)
Stack(
@@ -603,36 +607,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
.withValues(alpha: 0.6),
),
),
if (!enableTracing && isOutgoing)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(
context,
).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(10),
topRight: Radius.circular(8),
),
),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
),
),
],
)
else
@@ -650,30 +624,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
originalStyle: TextStyle(
fontSize: bodyFontSize * textScale,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.72),
),
),
),
if (!enableTracing && isOutgoing) ...[
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
),
],
],
),
if (enableTracing) ...[
if (displayPath.isNotEmpty) ...[
if (enableTracing && displayPath.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
padding: gifId != null
@@ -687,8 +647,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
fontSize: 11 * textScale,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
),
@@ -708,48 +670,54 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text(
_formatTime(context, message.timestamp),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
fontSize: 11 * textScale,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
if (message.repeatCount > 0) ...[
if (enableTracing && message.repeatCount > 0) ...[
const SizedBox(width: 6),
Icon(
Icons.repeat,
size: 12,
color: Colors.grey[600],
size: 12 * textScale,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 2),
Text(
'${message.repeatCount}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
fontSize: 11 * textScale,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
if (isOutgoing) ...[
const SizedBox(width: 4),
Icon(
message.status == ChannelMessageStatus.sent
? Icons.check
: message.status ==
ChannelMessageStatus.pending
? Icons.schedule
: Icons.error_outline,
size: 14,
color:
MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.failed
? Colors.red
: Colors.grey[600],
ChannelMessageStatus.sent,
isRepeated:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isPending:
message.status ==
ChannelMessageStatus.pending,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
],
],
),
),
],
],
),
),
),
@@ -764,6 +732,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
],
],
),
);
if (!isOutgoing && !PlatformInfo.isDesktop) {
@@ -965,9 +934,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
IconButton(
icon: Icon(Icons.location_on_outlined, color: channelColor),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
onPressed: () {
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
final selfName =
context.read<MeshCoreConnector>().selfName ??
context.l10n.chat_me;
final fromName = isOutgoing ? selfName : senderName;
final key = buildSharedMarkerKey(
sourceId: 'channel:${widget.channel.index}',
@@ -1027,8 +998,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
Widget _buildAvatar(String senderName) {
final initial = _getFirstCharacterOrEmoji(senderName);
final color = _getColorForName(senderName);
final initial = firstCharacterOrEmoji(senderName);
final color = colorForName(senderName);
return CircleAvatar(
radius: 18,
@@ -1044,36 +1015,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
String _getFirstCharacterOrEmoji(String name) {
if (name.isEmpty) return '?';
final emoji = firstEmoji(name);
if (emoji != null) return emoji;
final runes = name.runes.toList();
if (runes.isEmpty) return '?';
return String.fromCharCode(runes[0]).toUpperCase();
}
Color _getColorForName(String name) {
// Generate a consistent color based on the name hash
final hash = name.hashCode;
final colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.amber,
Colors.deepOrange,
];
return colors[hash.abs() % colors.length];
}
Widget _buildReplyBanner(double textScale) {
final message = _replyingToMessage!;
return Container(
@@ -1123,8 +1064,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
icon: const Icon(Icons.close, size: 18),
onPressed: _cancelReply,
color: Theme.of(context).colorScheme.onSecondaryContainer,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
),
],
),
@@ -1419,7 +1359,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_setReplyingTo(message);
},
),
if (PlatformInfo.isDesktop)
ListTile(
leading: const Icon(Icons.route),
title: Text(context.l10n.chat_path),
@@ -1470,19 +1409,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_markAsUnread(message);
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.delete_outline),
title: Text(context.l10n.common_delete),
leading: Icon(
Icons.delete_outline,
color: Theme.of(context).colorScheme.error,
),
title: Text(
context.l10n.common_delete,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
onTap: () async {
Navigator.pop(sheetContext);
await _deleteMessage(message);
},
),
ListTile(
leading: const Icon(Icons.close),
title: Text(context.l10n.common_cancel),
onTap: () => Navigator.pop(sheetContext),
),
],
),
),
@@ -1523,6 +1464,34 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
Future<void> _confirmClearChat() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.contact_clearChat),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () => Navigator.pop(dialogContext, true),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
child: Text(context.l10n.common_delete),
),
],
),
);
if (confirmed == true) {
if (!mounted) return;
context.read<MeshCoreConnector>().clearMessagesForChannel(
widget.channel.index,
);
}
}
Future<void> _deleteMessage(ChannelMessage message) async {
await context.read<MeshCoreConnector>().deleteChannelMessage(message);
if (!mounted) return;
@@ -1565,6 +1534,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
double _maxSwipeDistance = 0;
int? _swipePointerId;
bool _swipeLockedToHorizontal = false;
bool _isRtl = false;
void _handleSwipeStart(Offset position) {
_swipeStartPosition = position;
@@ -1585,11 +1555,13 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
return;
}
final dx = event.position.dx - _swipeStartPosition!.dx;
final rawDx = event.position.dx - _swipeStartPosition!.dx;
// In LTR swipe left (rawDx < 0) triggers reply; in RTL swipe right (rawDx > 0).
final signedDx = _isRtl ? rawDx : -rawDx;
const axisLockThreshold = 12.0;
if (!_swipeLockedToHorizontal) {
if (-dx < axisLockThreshold) {
if (signedDx < axisLockThreshold) {
return;
}
_swipeLockedToHorizontal = true;
@@ -1601,28 +1573,32 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
void _handleSwipeUpdate(Offset position) {
if (_swipeStartPosition == null) return;
final dx = position.dx - _swipeStartPosition!.dx;
if (dx >= 0) return;
final rawDx = position.dx - _swipeStartPosition!.dx;
final signedDx = _isRtl ? rawDx : -rawDx;
if (signedDx <= 0) return;
if (-dx < 6) return;
if (signedDx < 6) return;
if (-dx > _maxSwipeDistance) {
_maxSwipeDistance = -dx;
if (signedDx > _maxSwipeDistance) {
_maxSwipeDistance = signedDx;
}
final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble();
final double clamped = signedDx.clamp(0.0, widget.maxSwipeOffset);
final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
if (adjusted != _swipeOffset) {
setState(() => _swipeOffset = adjusted);
// Translate in the gesture direction: negative for LTR (left), positive for RTL (right).
final translationOffset = _isRtl ? adjusted : -adjusted;
if (translationOffset != _swipeOffset) {
setState(() => _swipeOffset = translationOffset);
}
}
void _handleSwipePointerUp(Offset position) {
if (_swipeLockedToHorizontal && _swipeStartPosition != null) {
final dx = position.dx - _swipeStartPosition!.dx;
final rawDx = position.dx - _swipeStartPosition!.dx;
final signedDx = _isRtl ? rawDx : -rawDx;
final peak = math.max(
_maxSwipeDistance,
(-dx).clamp(0.0, double.infinity),
signedDx.clamp(0.0, double.infinity),
);
if (peak >= widget.replySwipeThreshold) {
widget.onReplyTriggered();
@@ -1662,6 +1638,10 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
@override
Widget build(BuildContext context) {
_isRtl = Directionality.of(context) == TextDirection.rtl;
// In LTR, the bubble slides left and the hint appears on the right (isStart: false).
// In RTL, the bubble slides right and the hint appears on the left (isStart: true).
final hintIsStart = _isRtl;
return Listener(
onPointerDown: _handleSwipePointerDown,
onPointerMove: _handleSwipePointerMove,
@@ -1675,7 +1655,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
Positioned.fill(
child: Opacity(
opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
child: widget.hintBuilder(isStart: false),
child: widget.hintBuilder(isStart: hintIsStart),
),
),
AnimatedContainer(
+66 -13
View File
@@ -128,7 +128,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
if (!hasHopDetails)
Text(
l10n.channelPath_noHopDetails,
style: const TextStyle(color: Colors.grey),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
else
..._buildHopTiles(context, hops),
@@ -157,22 +159,33 @@ class ChannelMessagePathScreen extends StatelessWidget {
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
_buildDetailRow(l10n.channelPath_senderLabel, message.senderName),
_buildDetailRow(
context,
l10n.channelPath_senderLabel,
message.senderName,
),
_buildDetailRow(
context,
l10n.channelPath_timeLabel,
_formatTime(message.timestamp, l10n),
),
if (message.repeatCount > 0)
_buildDetailRow(
context,
l10n.channelPath_repeatsLabel,
message.repeatCount.toString(),
),
_buildDetailRow(
context,
l10n.channelPath_pathLabelTitle,
_formatPathLabel(effectiveHopCount, l10n),
),
if (observedLabel != null)
_buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
_buildDetailRow(
context,
l10n.channelPath_observedLabel,
observedLabel,
),
],
),
),
@@ -280,7 +293,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
return l10n.channelPath_observedSomeOf(observedCount, targetHopCount);
}
Widget _buildDetailRow(String label, String value) {
Widget _buildDetailRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
@@ -288,7 +301,12 @@ class ChannelMessagePathScreen extends StatelessWidget {
children: [
SizedBox(
width: 70,
child: Text(label, style: TextStyle(color: Colors.grey[600])),
child: Text(
label,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
Expanded(child: Text(value)),
],
@@ -442,17 +460,17 @@ class _ChannelMessagePathMapScreenState
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Zoom in',
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1),
),
IconButton(
icon: const Icon(Icons.remove),
tooltip: 'Zoom out',
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1),
),
IconButton(
icon: const Icon(Icons.my_location),
tooltip: 'Center map',
tooltip: context.l10n.map_centerMap,
onPressed: () => _resetMapView(
initialCenter: initialCenter,
initialZoom: initialZoom,
@@ -630,7 +648,9 @@ class _ChannelMessagePathMapScreenState
if (points.isEmpty)
Center(
child: Card(
color: Colors.white.withValues(alpha: 0.9),
color: Theme.of(
context,
).colorScheme.surface.withValues(alpha: 0.9),
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
@@ -703,7 +723,10 @@ class _ChannelMessagePathMapScreenState
label,
_formatPathPrefixes(selectedPath.pathBytes, width),
),
style: TextStyle(color: Colors.grey[700], fontSize: 12),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 12,
),
),
],
),
@@ -724,9 +747,12 @@ class _ChannelMessagePathMapScreenState
markers.add(
Marker(
point: point,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
child: Container(
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
@@ -750,6 +776,7 @@ class _ChannelMessagePathMapScreenState
),
),
),
),
);
if (showLabels) {
markers.add(
@@ -768,9 +795,12 @@ class _ChannelMessagePathMapScreenState
markers.add(
Marker(
point: selfPoint,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
child: Container(
decoration: BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
@@ -794,6 +824,7 @@ class _ChannelMessagePathMapScreenState
),
),
),
),
);
if (showLabels) {
markers.add(
@@ -843,6 +874,12 @@ class _ChannelMessagePathMapScreenState
);
}
Widget _colorDot(Color color) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
Widget _buildLegendCard(
BuildContext context,
List<_PathHop> hops,
@@ -865,10 +902,26 @@ class _ChannelMessagePathMapScreenState
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Text(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
Row(
children: [
_colorDot(Colors.green),
const SizedBox(width: 4),
Text(
l10n.pathTrace_legendGpsConfirmed,
style: const TextStyle(fontSize: 11),
),
],
),
],
),
),
const Divider(height: 1),
Expanded(
+96 -47
View File
@@ -111,14 +111,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
Icon(
Icons.logout,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context),
),
if (_communities.isNotEmpty)
PopupMenuItem(
child: Row(
children: [
@@ -241,32 +243,21 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
Expanded(
child: filteredChannels.isEmpty
? ListView(
? LayoutBuilder(
builder: (context, constraints) => ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(
height: MediaQuery.of(context).size.height - 300,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
const SizedBox(height: 16),
Text(
context.l10n.channels_noChannelsFound,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
child: EmptyState(
icon: Icons.search_off,
title: context.l10n.channels_noChannelsFound,
),
),
],
),
),
),
],
)
: (viewState.channelsSortOption ==
ChannelSortOption.manual &&
@@ -355,6 +346,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
int? dragIndex,
}) {
final unreadCount = connector.getUnreadCountForChannel(channel);
final isMuted = context.watch<AppSettingsService>().isChannelMuted(
channel.name,
);
// Determine icon and colors based on channel type
IconData icon;
@@ -447,27 +441,36 @@ class _ChannelsScreenState extends State<ChannelsScreen>
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isMuted) ...[
Icon(
Icons.notifications_off,
size: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
],
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(width: 4),
],
if (showDragHandle && dragIndex != null)
ReorderableDelayedDragStartListener(
ReorderableDragStartListener(
index: dragIndex,
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(
Icons.drag_handle,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
onTap: () async {
onTap: () {
final unread = connector.getUnreadCountForChannelIndex(
channel.index,
);
connector.markChannelRead(channel.index);
await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(
@@ -477,7 +480,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
),
);
}
},
onLongPress: () => _showChannelActions(
context,
@@ -538,10 +540,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
},
),
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),
leading: Icon(
Icons.delete_outline,
color: Theme.of(sheetContext).colorScheme.error,
),
title: Text(
context.l10n.channels_deleteChannel,
style: const TextStyle(color: Colors.red),
style: TextStyle(
color: Theme.of(sheetContext).colorScheme.error,
),
),
onTap: () async {
Navigator.pop(sheetContext);
@@ -723,23 +730,39 @@ class _ChannelsScreenState extends State<ChannelsScreen>
? (isSelected
? Theme.of(dialogContext).colorScheme.primaryContainer
: null)
: Colors.grey.withValues(alpha: 0.2),
: Theme.of(
dialogContext,
).colorScheme.onSurface.withValues(alpha: 0.12),
child: Icon(
icon,
color: enabled
? (isSelected
? Theme.of(dialogContext).colorScheme.primary
: null)
: Colors.grey,
: Theme.of(
dialogContext,
).colorScheme.onSurface.withValues(alpha: 0.38),
),
),
title: Text(
title,
style: TextStyle(color: enabled ? null : Colors.grey),
style: TextStyle(
color: enabled
? null
: Theme.of(
dialogContext,
).colorScheme.onSurface.withValues(alpha: 0.38),
),
),
subtitle: Text(
subtitle,
style: TextStyle(color: enabled ? null : Colors.grey),
style: TextStyle(
color: enabled
? null
: Theme.of(
dialogContext,
).colorScheme.onSurface.withValues(alpha: 0.38),
),
),
trailing: enabled ? const Icon(Icons.chevron_right) : null,
selected: isSelected,
@@ -927,7 +950,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Channel.publicChannelPsk,
);
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, 'Public', psk);
connector.setChannel(
nextIndex,
context.l10n.channels_public,
psk,
);
if (context.mounted) {
showDismissibleSnackBar(
context,
@@ -1041,7 +1068,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
dialogContext.l10n.community_hashtagPrivacyHint,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
color: Theme.of(
dialogContext,
).colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
@@ -1212,6 +1241,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: FilledButton(
onPressed: () async {
final name = nameController.text.trim();
final publicLabel =
context.l10n.channels_public;
if (name.isEmpty) {
showDismissibleSnackBar(
context,
@@ -1236,7 +1267,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final psk = community
.deriveCommunityPublicPsk();
final channelName =
'${community.name} Public';
'${community.name} $publicLabel';
connector.setChannel(
nextIndex,
channelName,
@@ -1592,7 +1623,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
},
child: Text(
dialogContext.l10n.common_delete,
style: const TextStyle(color: Colors.red),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
@@ -1602,7 +1633,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
final psk = Channel.parsePskHex(Channel.publicChannelPsk);
connector.setChannel(0, 'Public', psk);
connector.setChannel(0, context.l10n.channels_public, psk);
showDismissibleSnackBar(
context,
content: Text(context.l10n.channels_publicChannelAdded),
@@ -1651,14 +1682,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Icon(
Icons.groups_outlined,
size: 64,
color: Colors.grey[400],
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.6),
),
const SizedBox(height: 16),
Text(
context.l10n.community_noCommunities,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
@@ -1666,7 +1702,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
context.l10n.community_scanOrCreate,
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.8),
),
textAlign: TextAlign.center,
),
@@ -1690,10 +1729,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
title: Text(community.name),
subtitle: Text(
'ID: ${community.shortCommunityId}...',
context.l10n.channels_communityShortId(
community.shortCommunityId,
),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
trailing: PopupMenuButton<String>(
@@ -1720,14 +1763,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
value: 'leave',
child: Row(
children: [
const Icon(
Icon(
Icons.exit_to_app,
color: Colors.red,
color: Theme.of(
context,
).colorScheme.error,
),
const SizedBox(width: 12),
Text(
context.l10n.community_delete,
style: const TextStyle(color: Colors.red),
style: TextStyle(
color: Theme.of(
context,
).colorScheme.error,
),
),
],
),
@@ -1828,7 +1877,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
},
child: Text(
dialogContext.l10n.community_delete,
style: const TextStyle(color: Colors.red),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
File diff suppressed because it is too large Load Diff
+18 -21
View File
@@ -8,34 +8,25 @@ class ChromeRequiredScreen extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final colorScheme = theme.colorScheme;
return Scaffold(
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
),
),
color: colorScheme.surface,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
color: colorScheme.tertiaryContainer.withValues(alpha: 0.4),
shape: BoxShape.circle,
),
child: const Icon(
child: Icon(
Icons.browser_not_supported_rounded,
size: 80,
color: Colors.orange,
color: colorScheme.tertiary,
),
),
const SizedBox(height: 32),
@@ -44,7 +35,7 @@ class ChromeRequiredScreen extends StatelessWidget {
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 16),
@@ -52,7 +43,7 @@ class ChromeRequiredScreen extends StatelessWidget {
l10n.scanner_chromeRequiredMessage,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.white70 : Colors.black54,
color: colorScheme.onSurfaceVariant,
height: 1.5,
),
),
@@ -62,19 +53,25 @@ class ChromeRequiredScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
color: colorScheme.secondaryContainer.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
border: Border.all(
color: colorScheme.outline.withValues(alpha: 0.4),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
Icon(
Icons.info_outline,
size: 20,
color: colorScheme.secondary,
),
const SizedBox(width: 12),
Text(
"Web Bluetooth requires a Chromium browser",
l10n.chrome_bluetoothRequiresChromium,
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.blue,
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w500,
),
),
@@ -210,10 +210,10 @@ class _NoiseChartPainter extends CustomPainter {
}
final span = maxV - minV;
for (var i = 0; i <= 2; i++) {
final v = maxV - span * i / 2;
for (var i = 0; i <= 4; i++) {
final v = maxV - span * i / 4;
final tp = _yAxisLabel(v);
final y = chart.top + (chart.height * i / 2) - tp.height / 2;
final y = chart.top + (chart.height * i / 4) - tp.height / 2;
tp.paint(canvas, Offset(4, y));
}
+180 -128
View File
@@ -29,6 +29,7 @@ import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import '../widgets/sync_progress_overlay.dart';
import '../widgets/unread_badge.dart';
import '../helpers/contact_ui.dart';
import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
@@ -59,7 +60,7 @@ class _ContactsScreenState extends State<ContactsScreen>
String _loadedGroupScopeKeyHex = '';
Timer? _searchDebounce;
final Set<ContactOperationType> _pendingOperations = {};
final List<ContactOperationType> _pendingOperations = [];
StreamSubscription<Uint8List>? _frameSubscription;
@@ -185,59 +186,52 @@ class _ContactsScreenState extends State<ContactsScreen>
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
}
// Generic OK/ERR acks carry no command correlation, so consume only
// the oldest pending operation per ack instead of clearing all.
if (code == respCodeOk) {
// Show a snackbar indicating success
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
if (_pendingOperations.isEmpty) return;
final op = _pendingOperations.removeAt(0);
switch (op) {
case ContactOperationType.import:
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactImported),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
case ContactOperationType.zeroHopShare:
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
case ContactOperationType.export:
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactAdvertCopied),
);
}
_pendingOperations.clear();
}
if (code == respCodeErr) {
// Show a snackbar indicating failure
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
if (_pendingOperations.isEmpty) return;
final op = _pendingOperations.removeAt(0);
switch (op) {
case ContactOperationType.import:
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactImportFailed),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
case ContactOperationType.zeroHopShare:
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
case ContactOperationType.export:
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
);
}
_pendingOperations.clear();
}
} catch (e) {
appLogger.error(
@@ -252,17 +246,37 @@ class _ContactsScreenState extends State<ContactsScreen>
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactFrame = buildExportContactFrame(pubKey);
_pendingOperations.add(ContactOperationType.export);
try {
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
} catch (e) {
_pendingOperations.remove(ContactOperationType.export);
if (mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
);
}
}
}
Future<void> _contactZeroHop(Uint8List pubKey) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
_pendingOperations.add(ContactOperationType.zeroHopShare);
try {
await connector.sendFrame(
exportContactZeroHopFrame,
expectsGenericAck: true,
);
} catch (e) {
_pendingOperations.remove(ContactOperationType.zeroHopShare);
if (mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
);
}
}
}
Future<void> _contactImport() async {
@@ -288,11 +302,10 @@ class _ContactsScreenState extends State<ContactsScreen>
return;
}
final hexString = text.substring('meshcore://'.length);
final Uint8List importContactFrame;
try {
final bytes = hex2Uint8List(hexString);
final importContactFrame = buildImportContactFrame(bytes);
_pendingOperations.add(ContactOperationType.import);
connector.importContact(importContactFrame);
importContactFrame = buildImportContactFrame(bytes);
} catch (e) {
if (mounted) {
showDismissibleSnackBar(
@@ -300,6 +313,19 @@ class _ContactsScreenState extends State<ContactsScreen>
content: Text(context.l10n.contacts_invalidAdvertFormat),
);
}
return;
}
_pendingOperations.add(ContactOperationType.import);
try {
await connector.sendFrame(importContactFrame, expectsGenericAck: true);
} catch (e) {
_pendingOperations.remove(ContactOperationType.import);
if (mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactImportFailed),
);
}
}
}
@@ -322,7 +348,34 @@ class _ContactsScreenState extends State<ContactsScreen>
bottom: const SyncProgressAppBarBottom(),
actions: [
PopupMenuButton(
itemBuilder: (context) => [
tooltip: context.l10n.contacts_moreOptions,
itemBuilder: (context) => <PopupMenuEntry<dynamic>>[
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.person_add_rounded),
const SizedBox(width: 8),
Text(context.l10n.discoveredContacts_Title),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
),
onTap: () => _contactImport(),
),
const PopupMenuDivider(),
PopupMenuItem(
child: Row(
children: [
@@ -365,46 +418,20 @@ class _ContactsScreenState extends State<ContactsScreen>
),
onTap: () => _contactExport(Uint8List.fromList([])),
),
const PopupMenuDivider(),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
Icon(
Icons.logout,
color: Theme.of(context).colorScheme.error,
),
onTap: () => _contactImport(),
),
],
icon: const Icon(Icons.connect_without_contact),
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.person_add_rounded),
const SizedBox(width: 8),
Text(context.l10n.discoveredContacts_Title),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
),
),
PopupMenuItem(
child: Row(
children: [
@@ -426,6 +453,10 @@ class _ContactsScreenState extends State<ContactsScreen>
],
),
body: _buildContactsBody(context, connector),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddContactSheet(context),
child: const Icon(Icons.person_add),
),
bottomNavigationBar: SafeArea(
top: false,
child: QuickSwitchBar(
@@ -440,6 +471,40 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showAddContactSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.paste),
title: Text(context.l10n.contacts_addContactFromClipboard),
onTap: () {
Navigator.pop(sheetContext);
_contactImport();
},
),
ListTile(
leading: const Icon(Icons.person_add_rounded),
title: Text(context.l10n.discoveredContacts_Title),
onTap: () {
Navigator.pop(sheetContext);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
);
},
),
],
),
),
);
}
Future<void> _disconnect(
BuildContext context,
MeshCoreConnector connector,
@@ -571,7 +636,11 @@ class _ContactsScreenState extends State<ContactsScreen>
const SizedBox(width: 8),
IconButton(
tooltip: menuContext.l10n.contacts_deleteGroup,
icon: const Icon(Icons.delete, size: 20, color: Colors.red),
icon: Icon(
Icons.delete,
size: 20,
color: Theme.of(context).colorScheme.error,
),
onPressed: canManageGroups
? () => _closeDropdownAndRun(
menuContext,
@@ -589,12 +658,20 @@ class _ContactsScreenState extends State<ContactsScreen>
],
child: SizedBox(
height: 48,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
Expanded(
child: Text(selectedGroupName, overflow: TextOverflow.ellipsis),
child: Text(
selectedGroupName,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
const Icon(Icons.arrow_drop_down),
@@ -602,6 +679,7 @@ class _ContactsScreenState extends State<ContactsScreen>
),
),
),
),
);
}
@@ -624,6 +702,14 @@ class _ContactsScreenState extends State<ContactsScreen>
icon: Icons.people_outline,
title: context.l10n.contacts_noContacts,
subtitle: context.l10n.contacts_contactsWillAppear,
action: FilledButton.icon(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DiscoveryScreen()),
),
icon: const Icon(Icons.person_add_rounded),
label: Text(context.l10n.discoveredContacts_Title),
),
);
}
@@ -759,6 +845,9 @@ class _ContactsScreenState extends State<ContactsScreen>
width: 48,
height: 48,
child: IconButton(
tooltip: viewState.contactsSearchExpanded
? context.l10n.contacts_searchClose
: context.l10n.contacts_searchOpen,
onPressed: () {
if (viewState.contactsSearchExpanded) {
_collapseContactsSearch(viewState);
@@ -791,25 +880,29 @@ class _ContactsScreenState extends State<ContactsScreen>
),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => connector.getContacts(),
child: filteredAndSorted.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
? LayoutBuilder(
builder: (context, constraints) => ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
viewState.contactsShowUnreadOnly
ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: EmptyState(
icon: Icons.search_off,
title: viewState.contactsShowUnreadOnly
? context.l10n.contacts_noUnreadContacts
: context.l10n.contacts_noContactsFound,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
),
],
),
)
: RefreshIndicator(
onRefresh: () => connector.getContacts(),
child: ListView.builder(
: ListView.builder(
padding: const EdgeInsets.only(bottom: 88),
itemCount: filteredAndSorted.length,
itemBuilder: (context, index) {
final contact = filteredAndSorted[index];
@@ -1048,7 +1141,7 @@ class _ContactsScreenState extends State<ContactsScreen>
},
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
@@ -1362,14 +1455,6 @@ class _ContactsScreenState extends State<ContactsScreen>
);
},
),
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
onTap: () {
Navigator.pop(sheetContext);
_openChat(context, contact);
},
),
],
ListTile(
leading: Icon(
@@ -1406,10 +1491,13 @@ class _ContactsScreenState extends State<ContactsScreen>
},
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
leading: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.error,
),
title: Text(
context.l10n.contacts_deleteContact,
style: const TextStyle(color: Colors.red),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
onTap: () {
Navigator.pop(sheetContext);
@@ -1454,7 +1542,7 @@ class _ContactsScreenState extends State<ContactsScreen>
},
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
@@ -1486,26 +1574,15 @@ class _ContactTile extends StatelessWidget {
onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null,
child: ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
backgroundColor: contactTypeColor(contact.type),
child: _buildContactAvatar(contact),
),
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subtitle: Text(
contact.pathLabel(context.l10n),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
contact.shortPubKeyHex,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
],
),
// Clamp text scaling in trailing section to prevent overflow while
// maintaining accessibility. Primary content (title/subtitle) scales normally.
trailing: MediaQuery(
@@ -1515,7 +1592,7 @@ class _ContactTile extends StatelessWidget {
),
),
child: SizedBox(
width: 120,
width: 96,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
@@ -1529,7 +1606,10 @@ class _ContactTile extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Row(
mainAxisSize: MainAxisSize.min,
@@ -1542,7 +1622,9 @@ class _ContactTile extends StatelessWidget {
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[400],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
],
),
@@ -1561,37 +1643,7 @@ class _ContactTile extends StatelessWidget {
if (emoji != null) {
return Text(emoji, style: const TextStyle(fontSize: 18));
}
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
}
IconData _getTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color _getTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
return Icon(contactTypeIcon(contact.type), color: Colors.white, size: 20);
}
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
+28 -44
View File
@@ -12,6 +12,7 @@ import '../utils/contact_search.dart';
import '../utils/platform_info.dart';
import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart';
import '../helpers/contact_ui.dart';
import '../helpers/snack_bar_builder.dart';
enum DiscoverySortOption { lastSeen, name, type }
@@ -71,7 +72,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
Icon(
Icons.delete,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Text(context.l10n.discoveredContacts_deleteContactAll),
],
@@ -99,9 +103,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
final contact = filteredAndSorted[index];
final tile = ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
backgroundColor: contactTypeColor(contact.type),
child: Icon(
_getTypeIcon(contact.type),
contactTypeIcon(contact.type),
color: Colors.white,
size: 20,
),
@@ -142,7 +146,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
Row(
@@ -152,7 +158,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[400],
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.6),
),
if (contact.rawPacket != null)
const SizedBox(width: 2),
@@ -160,7 +169,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
Icon(
Icons.cell_tower,
size: 14,
color: Colors.grey[400],
color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.6),
),
],
),
@@ -170,6 +182,16 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
),
onTap: () {
connector.importDiscoveredContact(contact);
showDismissibleSnackBar(
context,
content: Text(
context.l10n.discoveredContacts_contactAdded,
),
action: SnackBarAction(
label: context.l10n.common_undo,
onPressed: () => connector.removeContact(contact),
),
);
},
onLongPress: () =>
_showContactContextMenu(contact, connector),
@@ -203,11 +225,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add_reaction_sharp),
title: Text(l10n.discoveredContacts_addContact),
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(l10n.discoveredContacts_copyContact),
@@ -227,9 +244,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
if (!mounted || action == null) return;
switch (action) {
case 'import_contact':
connector.importDiscoveredContact(contact);
break;
case 'copy_contact':
if (contact.rawPacket == null) return;
final hexString = pubKeyToHex(contact.rawPacket!);
@@ -429,36 +443,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
}
}
IconData _getTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color _getTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
}
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
final now = DateTime.now();
final diff = now.difference(lastSeen);
+42 -16
View File
@@ -73,7 +73,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
double _startAntennaHeight = 5.0;
double _endAntennaHeight = 5.0;
bool _showHud = true;
bool _menuExpanded = true;
bool _menuExpanded = false;
bool _showDisplayNodes = true;
bool _showMarkerLabels = true;
bool _didReceivePositionUpdate = false;
@@ -159,17 +159,17 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Zoom in',
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1),
),
IconButton(
icon: const Icon(Icons.remove),
tooltip: 'Zoom out',
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1),
),
IconButton(
icon: const Icon(Icons.my_location),
tooltip: 'Center map',
tooltip: context.l10n.map_centerMap,
onPressed: () => _resetMapView(
initialCenter: initialCenter,
initialZoom: initialZoom,
@@ -224,6 +224,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
setState(() {
_result = result;
_selectedObstruction = _defaultObstructionFor(result);
_menuExpanded = true;
});
} catch (e) {
if (!mounted) return;
@@ -506,7 +507,9 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
bottom: 12,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black54,
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
@@ -516,11 +519,21 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
),
child: Text(
context.l10n.losElevationAttribution,
style: const TextStyle(fontSize: 10, color: Colors.white),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
),
if (_loading)
const Positioned(
left: 0,
right: 0,
top: 0,
child: LinearProgressIndicator(),
),
],
),
floatingActionButton: FloatingActionButton(
@@ -623,7 +636,10 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
const SizedBox(height: 4),
Text(
context.l10n.losBlockedSpotsHint,
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 6),
Wrap(
@@ -692,7 +708,9 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
'${_selectedObstruction!.point.longitude.toStringAsFixed(5)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
@@ -711,14 +729,17 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
context.l10n.losFrequencyLabel,
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 8),
Text(
'${displayFrequencyMHz.toStringAsFixed(3)} MHz',
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (kFactorUsed != null) ...[
const SizedBox(width: 8),
@@ -726,7 +747,9 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
'k=${kFactorUsed.toStringAsFixed(3)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 4),
@@ -734,7 +757,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(Icons.info_outline, size: 16),
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
tooltip: context.l10n.losFrequencyInfoTooltip,
onPressed: () {
_showFrequencyInfoDialog(
@@ -750,7 +773,10 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
),
Text(
context.l10n.losElevationAttribution,
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 6),
ExpansionTile(
@@ -1730,12 +1756,12 @@ class _LosLegend extends StatelessWidget {
Widget build(BuildContext context) {
final textStyle =
Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white70,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 11,
fontWeight: FontWeight.w500,
) ??
const TextStyle(
color: Colors.white70,
TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 11,
fontWeight: FontWeight.w500,
);
+6 -4
View File
@@ -83,17 +83,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Zoom in',
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1),
),
IconButton(
icon: const Icon(Icons.remove),
tooltip: 'Zoom out',
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1),
),
IconButton(
icon: const Icon(Icons.my_location),
tooltip: 'Center map',
tooltip: context.l10n.map_centerMap,
onPressed: _resetMapView,
),
],
@@ -458,7 +458,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
padding: const EdgeInsets.only(top: 8),
child: Text(
l10n.mapCache_failedDownloads(_failedTiles),
style: TextStyle(color: Colors.orange[700]),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
+143 -92
View File
@@ -185,17 +185,17 @@ class _MapScreenState extends State<MapScreen> {
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Zoom in',
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1),
),
IconButton(
icon: const Icon(Icons.remove),
tooltip: 'Zoom out',
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1),
),
IconButton(
icon: const Icon(Icons.my_location),
tooltip: 'Center map',
tooltip: context.l10n.map_centerMap,
onPressed: () => _mapController.move(center, zoom),
),
],
@@ -420,18 +420,36 @@ class _MapScreenState extends State<MapScreen> {
automaticallyImplyLeading: false,
bottom: const SyncProgressAppBarBottom(),
actions: [
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(
LatLng(connector.selfLatitude!, connector.selfLongitude!),
PopupMenuButton(
itemBuilder: (context) => [
if (!_isBuildingPathTrace &&
connector.selfLatitude != null &&
connector.selfLongitude != null)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.radar),
const SizedBox(width: 8),
Text(context.l10n.contacts_pathTrace),
],
),
onTap: () => _startPath(
LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
IconButton(
icon: const LosIcon(),
onPressed: () {
PopupMenuItem(
child: Row(
children: [
const LosIcon(),
const SizedBox(width: 8),
Text(context.l10n.map_lineOfSight),
],
),
onTap: () {
final candidates = <LineOfSightEndpoint>[];
if (connector.selfLatitude != null &&
connector.selfLongitude != null) {
@@ -467,14 +485,14 @@ class _MapScreenState extends State<MapScreen> {
),
);
},
tooltip: context.l10n.map_lineOfSight,
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
Icon(
Icons.logout,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
@@ -907,8 +925,8 @@ class _MapScreenState extends State<MapScreen> {
final color = _getNodeColor(guess.contact.type);
final marker = Marker(
point: guess.position,
width: 35,
height: 35,
width: 48,
height: 48,
child: GestureDetector(
onLongPress: () => _isBuildingPathTrace
? _showNodeInfo(context, guess.contact)
@@ -920,6 +938,7 @@ class _MapScreenState extends State<MapScreen> {
guess.contact,
guessedPosition: guess.position,
),
child: Center(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
@@ -943,6 +962,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
),
),
);
markers.add(marker);
@@ -1041,17 +1061,16 @@ class _MapScreenState extends State<MapScreen> {
for (final contact in filteredContacts) {
final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 35,
height: 35,
width: 48,
height: 48,
child: GestureDetector(
onLongPress: () =>
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
onTap: () => _isBuildingPathTrace
? _addToPath(context, contact)
: _showNodeInfo(context, contact),
child: Column(
children: [
Container(
child: Center(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: settings.mapShowOverlaps && !_isBuildingPathTrace
@@ -1073,7 +1092,6 @@ class _MapScreenState extends State<MapScreen> {
size: 20,
),
),
],
),
),
);
@@ -1219,7 +1237,9 @@ class _MapScreenState extends State<MapScreen> {
Icon(
Icons.location_on,
size: 16,
color: Colors.grey,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
Text(
": $nodeCount",
@@ -1232,10 +1252,12 @@ class _MapScreenState extends State<MapScreen> {
),
Row(
children: [
const Icon(
Icon(
Icons.wrong_location,
size: 16,
color: Colors.grey,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
Text(
": ${nodeCountAll - nodeCount}",
@@ -1248,10 +1270,12 @@ class _MapScreenState extends State<MapScreen> {
),
Row(
children: [
const Icon(
Icon(
Icons.add_outlined,
size: 16,
color: Colors.grey,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
Text(
": $nodeCountAll",
@@ -1547,10 +1571,73 @@ class _MapScreenState extends State<MapScreen> {
LatLng? guessedPosition,
}) {
final connector = context.read<MeshCoreConnector>();
showDialog(
showModalBottomSheet(
context: context,
builder: (dialogContext) => AlertDialog(
title: Row(
showDragHandle: true,
builder: (sheetContext) {
final actions = <Widget>[];
if (contact.type == advTypeChat) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
final unread = connector.getUnreadCountForContactKey(
contact.publicKeyHex,
);
Navigator.pop(sheetContext);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
contact: contact,
initialUnreadCount: unread,
),
),
);
},
child: Text(context.l10n.contacts_openChat),
),
);
}
if (contact.type == advTypeRepeater) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(sheetContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
);
}
if (contact.type == advTypeRoom) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(sheetContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
);
}
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_getNodeIcon(contact.type),
@@ -1560,10 +1647,7 @@ class _MapScreenState extends State<MapScreen> {
Expanded(child: SelectableText(contact.name)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
_buildInfoRow(
context.l10n.map_type,
contact.typeLabel(context.l10n),
@@ -1586,61 +1670,22 @@ class _MapScreenState extends State<MapScreen> {
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
),
_buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
],
_buildInfoRow(
context.l10n.map_publicKey,
contact.publicKeyHex,
),
actions: [
const SizedBox(height: 16),
...actions,
TextButton(
onPressed: () => Navigator.pop(dialogContext),
onPressed: () => Navigator.pop(sheetContext),
child: Text(context.l10n.common_close),
),
if (contact.type ==
advTypeChat) // Only show chat button for chat nodes
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
final unread = connector.getUnreadCountForContactKey(
contact.publicKeyHex,
);
Navigator.pop(dialogContext);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
contact: contact,
initialUnreadCount: unread,
),
),
);
},
child: Text(context.l10n.contacts_openChat),
),
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
],
),
),
),
);
},
);
}
@@ -1725,6 +1770,9 @@ class _MapScreenState extends State<MapScreen> {
child: Text(context.l10n.common_hide),
),
TextButton(
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
onPressed: () async {
setState(() {
_hiddenMarkerIds.add(marker.id);
@@ -1756,7 +1804,7 @@ class _MapScreenState extends State<MapScreen> {
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
@@ -1821,10 +1869,7 @@ class _MapScreenState extends State<MapScreen> {
);
await connector.refreshDeviceInfo();
if (!mounted) return;
showDismissibleSnackBar(
messenger.context,
content: Text(successMsg),
);
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
},
),
ListTile(
@@ -2213,7 +2258,10 @@ class _MapScreenState extends State<MapScreen> {
const SizedBox(height: 8),
Text(
_getTimeFilterLabel(settings.mapTimeFilterHours),
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Slider(
value: _hoursToSliderValue(settings.mapTimeFilterHours),
@@ -2390,7 +2438,10 @@ class _MapScreenState extends State<MapScreen> {
if (_pathTrace.isNotEmpty)
Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
SelectableText(
PathHelper.splitPathBytes(
+15 -74
View File
@@ -9,7 +9,8 @@ import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/empty_state.dart';
import '../widgets/routing_sheet.dart';
import '../widgets/snr_indicator.dart';
import '../helpers/snack_bar_builder.dart';
@@ -167,7 +168,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
showDismissibleSnackBar(
context,
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
backgroundColor: Theme.of(context).colorScheme.tertiary,
);
_statusTimeout?.cancel();
if (!mounted) return;
@@ -227,7 +228,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
showDismissibleSnackBar(
context,
content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
_recordStatusResult(false);
});
@@ -241,7 +242,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
showDismissibleSnackBar(
context,
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
}
@@ -279,7 +280,9 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
children: [
Text(
l10n.neighbors_repeatersNeighbors,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
repeater.name,
@@ -287,75 +290,18 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
fontSize: 14,
fontWeight: FontWeight.normal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
ContactRoutingSheet.show(context, contact: repeater),
),
IconButton(
icon: _isLoading
@@ -380,12 +326,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
if (!_isLoaded &&
!_hasData &&
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
Center(
child: Text(
l10n.neighbors_noData,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
EmptyState(icon: Icons.wifi_find, title: l10n.neighbors_noData),
if (_isLoaded ||
_hasData &&
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
@@ -435,7 +376,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
fmtDuration(entry.value['lastHeard'] + 0.0),
),
entry.value['snr'],
connector.currentSf!,
connector.currentSf,
),
],
),
@@ -447,7 +388,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
String label,
String value,
double snr,
int spreadingFactor,
int? spreadingFactor,
) {
final snrUi = snrUiFromSNR(snr, spreadingFactor);
return Padding(
+89 -14
View File
@@ -119,6 +119,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
bool _showNodeLabels = true;
Contact? _targetContact;
Uint8List? _sentTagBytes;
// Live path resolved at trace time; used by the response handler for
// endpoint inference so it matches the path that was actually traced.
Uint8List _tracedPath = Uint8List(0);
String _formatPathPrefixes(Uint8List pathBytes, [int? hashByteWidth]) {
return PathHelper.splitPathBytes(
@@ -249,17 +252,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
children: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'Zoom in',
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1),
),
IconButton(
icon: const Icon(Icons.remove),
tooltip: 'Zoom out',
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1),
),
IconButton(
icon: const Icon(Icons.my_location),
tooltip: 'Center map',
tooltip: context.l10n.map_centerMap,
onPressed: _resetMapView,
),
],
@@ -326,6 +329,23 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
return Uint8List.fromList(traceBytes);
}
/// Resolves the path bytes to trace. When tracing a specific contact's
/// route (flipPathAround), re-read that contact's live forced/auto path from
/// the connector so a path the user just changed (force flood / set path /
/// reset to auto) is honored immediately, instead of the value captured when
/// this screen was first pushed.
Uint8List _resolveLivePath(MeshCoreConnector connector) {
final target = widget.targetContact;
if (!widget.flipPathAround || target == null) {
return widget.path;
}
final live = connector.allContactsUnfiltered.firstWhere(
(c) => c.publicKeyHex == target.publicKeyHex,
orElse: () => target,
);
return live.pathBytesForDisplay;
}
Future<void> _doPathTrace() async {
if (mounted) {
setState(() {
@@ -336,9 +356,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final traceHashByteWidth = _traceHashByteWidth(widget.pathHashByteWidth);
final livePath = _resolveLivePath(connector);
_tracedPath = livePath;
final pathTmp = widget.reversePathAround
? _reversePathByHop(widget.path, widget.pathHashByteWidth)
: widget.path;
? _reversePathByHop(livePath, widget.pathHashByteWidth)
: livePath;
final path = widget.flipPathAround
? buildPath(pathTmp, traceHashByteWidth, connector)
@@ -461,8 +484,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}
final pathBytes = buffer.readBytes(pathLength);
final pathData = PathHelper.splitPathBytes(pathBytes, width);
// Firmware emits (path_len >> path_sz) hop SNRs plus 1 final SNR (to this node).
final snrCount = (pathLength ~/ width) + 1;
List<double> snrData = buffer
.readRemainingBytes()
.readBytes(snrCount)
.map((snr) => snr.toSigned(8).toDouble() / 4)
.toList();
@@ -559,13 +584,20 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final tc = _targetContact!;
if (tc.hasLocation) {
targetPos = LatLng(tc.latitude!, tc.longitude!);
} else if (pathData.length > 1) {
} else if (pathData.length > 1 || _tracedPath.isNotEmpty) {
// Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
// sits in the middle of the symmetric sequence; .last is the local side.
final tracedHops = PathHelper.splitPathBytes(
_tracedPath,
widget.pathHashByteWidth,
);
final hopsForEndpoint = tracedHops.isNotEmpty
? tracedHops
: pathData;
final lastHop = widget.reversePathAround
? pathData.first
: pathData.last;
? hopsForEndpoint.first
: hopsForEndpoint.last;
final lastHopKey = _hopKey(lastHop);
final peers = connector.allContacts
@@ -743,7 +775,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
!_failed2Loaded)
Center(
child: Card(
color: Colors.white.withValues(alpha: 0.9),
color: Theme.of(
context,
).colorScheme.surface.withValues(alpha: 0.9),
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
@@ -795,9 +829,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add(
Marker(
point: point,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: hasGps
@@ -824,6 +861,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
),
);
if (showLabels) {
markers.add(_buildNodeLabelMarker(point: point, label: fullLabel));
@@ -839,9 +877,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add(
Marker(
point: selfPoint,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.blue,
@@ -866,6 +907,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
),
);
if (showLabels) {
markers.add(
@@ -885,9 +927,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add(
Marker(
point: targetPos,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isGuessed
@@ -907,6 +952,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
child: const Icon(Icons.person, color: Colors.white, size: 18),
),
),
),
);
if (showLabels) {
markers.add(
@@ -1077,6 +1123,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
);
}
Widget _colorDot(Color color) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
Widget _buildLegendCard(
BuildContext context,
PathTraceData pathTraceData,
@@ -1099,10 +1151,33 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Text(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
Row(
children: [
_colorDot(Colors.green),
const SizedBox(width: 4),
Text(
l10n.pathTrace_legendGpsConfirmed,
style: const TextStyle(fontSize: 11),
),
const SizedBox(width: 12),
_colorDot(Colors.orange),
const SizedBox(width: 4),
Text(
l10n.pathTrace_legendInferred,
style: const TextStyle(fontSize: 11),
),
],
),
],
),
),
const Divider(height: 1),
Expanded(
+50 -81
View File
@@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../widgets/debug_frame_viewer.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/routing_sheet.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterCliScreen extends StatefulWidget {
@@ -252,97 +252,29 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.repeater_cliTitle),
Text(
l10n.repeater_cliTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
repeater.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
IconButton(
icon: const Icon(Icons.bug_report),
tooltip: l10n.repeater_debugNextCommand,
onPressed: () {
// Set a flag or just send next command with debug
if (_commandController.text.trim().isNotEmpty) {
_sendCommand(showDebug: true);
} else {
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_enterCommandFirst),
);
}
},
ContactRoutingSheet.show(context, contact: repeater),
),
IconButton(
icon: const Icon(Icons.help_outline),
@@ -354,6 +286,33 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
tooltip: l10n.repeater_clearHistory,
onPressed: _commandHistory.isEmpty ? null : _clearHistory,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'debug') {
if (_commandController.text.trim().isNotEmpty) {
_sendCommand(showDebug: true);
} else {
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_enterCommandFirst),
);
}
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'debug',
child: Row(
children: [
const Icon(Icons.bug_report),
const SizedBox(width: 8),
Text(l10n.repeater_debugNextCommand),
],
),
),
],
),
],
),
body: Column(
@@ -426,16 +385,26 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.terminal, size: 64, color: Colors.grey[400]),
Icon(
Icons.terminal,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
l10n.repeater_noCommandsSent,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
l10n.repeater_typeCommandOrUseQuick,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
+34 -14
View File
@@ -72,11 +72,15 @@ class RepeaterHubScreen extends StatelessWidget {
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(
backgroundColor: Theme.of(
context,
).colorScheme.tertiaryContainer,
child: Icon(
Icons.cell_tower,
size: 40,
color: Colors.white,
color: Theme.of(
context,
).colorScheme.onTertiaryContainer,
),
),
const SizedBox(height: 16),
@@ -90,12 +94,18 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 8),
Text(
repeater.shortPubKeyHex,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Text(
repeater.pathLabel(context.l10n),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
@@ -105,14 +115,18 @@ class RepeaterHubScreen extends StatelessWidget {
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[600],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
@@ -193,7 +207,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
color: Theme.of(context).colorScheme.primary,
onTap: () {
Navigator.push(
context,
@@ -213,7 +227,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
color: Theme.of(context).colorScheme.secondary,
onTap: () {
Navigator.push(
context,
@@ -231,7 +245,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
color: Theme.of(context).colorScheme.tertiary,
onTap: () {
Navigator.push(
context,
@@ -251,7 +265,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.group,
title: l10n.repeater_neighbors,
subtitle: l10n.repeater_neighborsSubtitle,
color: Colors.orange,
color: Theme.of(context).colorScheme.tertiary,
onTap: () {
Navigator.push(
context,
@@ -270,7 +284,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
color: Theme.of(context).colorScheme.error,
onTap: () {
Navigator.push(
context,
@@ -329,12 +343,18 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(Icons.chevron_right, color: Colors.grey[400]),
Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
+50 -79
View File
@@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart';
import '../services/storage_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/routing_sheet.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterSettingsScreen extends StatefulWidget {
@@ -126,6 +126,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// Location settings
final TextEditingController _latController = TextEditingController();
final TextEditingController _lonController = TextEditingController();
bool _latInvalid = false;
bool _lonInvalid = false;
// Feature toggles
bool _repeatEnabled = true;
@@ -457,7 +459,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
? l10n.repeater_refreshed(label)
: l10n.repeater_errorRefreshing(label),
),
backgroundColor: successCount > 0 ? Colors.green : Colors.red,
backgroundColor: successCount > 0
? null
: Theme.of(context).colorScheme.error,
);
setState(() => setRefreshing(false));
}
@@ -667,15 +671,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
: l10n.repeater_actionSucceeded(label),
),
backgroundColor: outcome == _SaveOutcome.error
? Colors.red
: Colors.green,
? Theme.of(context).colorScheme.error
: null,
);
} catch (e) {
if (!mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_actionFailed(label, e.toString())),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
} finally {
if (mounted) setState(() => _runningAction = false);
@@ -768,14 +772,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
}
if (_dirtyFields.contains(_SettingField.lat) &&
_latController.text.isNotEmpty) {
_latController.text.isNotEmpty &&
_isValidCoordinate(_latController.text, 90)) {
pending.add((
field: _SettingField.lat,
command: 'set lat ${_latController.text}',
));
}
if (_dirtyFields.contains(_SettingField.lon) &&
_lonController.text.isNotEmpty) {
_lonController.text.isNotEmpty &&
_isValidCoordinate(_lonController.text, 180)) {
pending.add((
field: _SettingField.lon,
command: 'set lon ${_lonController.text}',
@@ -944,13 +950,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_settingsSavedRebootNeeded),
backgroundColor: Colors.orange,
backgroundColor: Theme.of(context).colorScheme.tertiary,
);
} else if (failures.isEmpty) {
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_settingsSaved),
backgroundColor: Colors.green,
);
} else {
showDismissibleSnackBar(
@@ -958,7 +963,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
content: Text(
l10n.repeater_settingsPartialFailure(failures.join('; ')),
),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
}
@@ -973,7 +978,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
content: Text(
context.l10n.repeater_errorSavingSettings(e.toString()),
),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
}
@@ -984,6 +989,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_flagHasChanges();
}
static bool _isValidCoordinate(String text, double max) {
if (text.trim().isEmpty) return true;
final value = double.tryParse(text.trim());
return value != null && value >= -max && value <= max;
}
void _flagHasChanges() {
if (!_hasChanges) {
setState(() {
@@ -1072,73 +1083,11 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
if (mounted) {
setState(() {});
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
ContactRoutingSheet.show(context, contact: repeater),
),
if (_hasChanges)
TextButton.icon(
@@ -1173,6 +1122,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
const SizedBox(height: 16),
_buildAdvancedCard(),
const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 16),
_buildDangerZoneCard(),
],
),
@@ -1388,13 +1339,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
decoration: InputDecoration(
labelText: l10n.repeater_latitude,
helperText: l10n.repeater_latitudeHelper,
errorText: _latInvalid
? l10n.settings_locationInvalid
: null,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
onChanged: (_) => _markChanged(_SettingField.lat),
onChanged: (value) {
_markChanged(_SettingField.lat);
final invalid = !_isValidCoordinate(value, 90);
if (invalid != _latInvalid) {
setState(() => _latInvalid = invalid);
}
},
),
),
const SizedBox(width: 8),
@@ -1415,13 +1375,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
decoration: InputDecoration(
labelText: l10n.repeater_longitude,
helperText: l10n.repeater_longitudeHelper,
errorText: _lonInvalid
? l10n.settings_locationInvalid
: null,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
onChanged: (_) => _markChanged(_SettingField.lon),
onChanged: (value) {
_markChanged(_SettingField.lon);
final invalid = !_isValidCoordinate(value, 180);
if (invalid != _lonInvalid) {
setState(() => _lonInvalid = invalid);
}
},
),
),
const SizedBox(width: 8),
@@ -2233,7 +2202,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
}
@@ -2262,7 +2231,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
onConfirm();
},
style: isDestructive
? FilledButton.styleFrom(backgroundColor: Colors.red)
? FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
)
: null,
child: Text(l10n.repeater_confirm),
),
+17 -71
View File
@@ -11,7 +11,7 @@ import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../utils/battery_utils.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/routing_sheet.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterStatusScreen extends StatefulWidget {
@@ -318,7 +318,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
showDismissibleSnackBar(
context,
content: Text(context.l10n.repeater_statusRequestTimeout),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
_recordStatusResult(false);
});
@@ -331,7 +331,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
showDismissibleSnackBar(
context,
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
_recordStatusResult(false);
@@ -360,82 +360,29 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.repeater_statusTitle),
Text(
l10n.repeater_statusTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
repeater.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
ContactRoutingSheet.show(context, contact: repeater),
),
IconButton(
icon: _isLoading
@@ -588,21 +535,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
Expanded(
child: Text(
label,
style: TextStyle(
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Text(
const SizedBox(width: 8),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.w400),
),
textAlign: TextAlign.end,
),
],
),
+75 -74
View File
@@ -10,6 +10,7 @@ import '../services/linux_ble_error_classifier.dart';
import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
import '../widgets/empty_state.dart';
import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart';
import 'tcp_screen.dart';
@@ -25,6 +26,7 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false;
String? _connectingDeviceId;
late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
@@ -101,6 +103,32 @@ class _ScannerScreenState extends State<ScannerScreen> {
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
if (PlatformInfo.supportsUsbSerial)
IconButton(
icon: const Icon(Icons.usb),
tooltip: context.l10n.connectionChoiceUsbLabel,
onPressed: () {
appLogger.info(
'USB selected, opening UsbScreen',
tag: 'ScannerScreen',
);
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const UsbScreen()));
},
),
if (!PlatformInfo.isWeb)
IconButton(
icon: const Icon(Icons.lan),
tooltip: context.l10n.connectionChoiceTcpLabel,
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const TcpScreen()));
},
),
],
),
body: SafeArea(
top: false,
@@ -122,69 +150,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
},
),
),
bottomNavigationBar: Consumer<MeshCoreConnector>(
floatingActionButton: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isScanning =
connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final usbSupported = PlatformInfo.supportsUsbSerial;
final tcpSupported = !PlatformInfo.isWeb;
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (usbSupported)
FloatingActionButton.extended(
onPressed: () {
appLogger.info(
'USB selected, opening UsbScreen',
tag: 'ScannerScreen',
);
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'scanner_usb_action',
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
if (usbSupported) const SizedBox(width: 12),
if (tcpSupported)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const TcpScreen()),
);
},
heroTag: 'scanner_tcp_action',
icon: const Icon(Icons.lan),
label: Text(context.l10n.connectionChoiceTcpLabel),
),
if (tcpSupported) const SizedBox(width: 12),
FloatingActionButton.extended(
return FloatingActionButton.extended(
heroTag: 'scanner_ble_action',
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
appLogger.warn(
'startScan error: $e',
tag: 'ScannerScreen',
);
}),
);
}
},
onPressed: isBluetoothOff ? null : () => _toggleScan(connector),
icon: isScanning
? const SizedBox(
width: 20,
@@ -197,16 +171,24 @@ class _ScannerScreenState extends State<ScannerScreen> {
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
),
],
),
),
);
},
),
);
}
void _toggleScan(MeshCoreConnector connector) {
if (connector.state == MeshCoreConnectionState.scanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
appLogger.warn('startScan error: $e', tag: 'ScannerScreen');
}),
);
}
}
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
String statusText;
Color statusColor;
@@ -254,32 +236,42 @@ class _ScannerScreenState extends State<ScannerScreen> {
Widget _buildDeviceList(BuildContext context, MeshCoreConnector connector) {
if (connector.scanResults.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
connector.state == MeshCoreConnectionState.scanning
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final isScanning = connector.state == MeshCoreConnectionState.scanning;
return EmptyState(
icon: isBluetoothOff ? Icons.bluetooth_disabled : Icons.bluetooth,
title: isBluetoothOff
? context.l10n.scanner_bluetoothOff
: isScanning
? context.l10n.scanner_searchingDevices
: context.l10n.scanner_tapToScan,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
subtitle: isBluetoothOff
? context.l10n.scanner_bluetoothOffMessage
: null,
action: (isBluetoothOff || isScanning)
? null
: FilledButton.icon(
onPressed: () => _toggleScan(connector),
icon: const Icon(Icons.bluetooth_searching),
label: Text(context.l10n.scanner_scan),
),
);
}
final isConnecting = connector.state == MeshCoreConnectionState.connecting;
return ListView.separated(
padding: const EdgeInsets.all(8),
itemCount: connector.scanResults.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
final result = connector.scanResults[index];
final deviceId = result.device.remoteId.toString();
return DeviceTile(
scanResult: result,
onTap: () => _connectToDevice(context, connector, result),
isConnecting: isConnecting && _connectingDeviceId == deviceId,
onTap: isConnecting
? null
: () => _connectToDevice(context, connector, result),
);
},
);
@@ -293,6 +285,9 @@ class _ScannerScreenState extends State<ScannerScreen> {
final name = result.device.platformName.isNotEmpty
? result.device.platformName
: result.advertisementData.advName;
setState(() {
_connectingDeviceId = result.device.remoteId.toString();
});
try {
await connector.connect(
result.device,
@@ -321,9 +316,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
showDismissibleSnackBar(
context,
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
} finally {
if (mounted) {
setState(() {
_connectingDeviceId = null;
});
}
}
}
+95 -34
View File
@@ -53,6 +53,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _loadVersionInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
if (!mounted) return;
setState(() {
_appVersion = packageInfo.version;
});
@@ -213,12 +214,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (percent == null) {
icon = Icons.battery_unknown;
iconColor = Colors.grey;
iconColor = Theme.of(context).colorScheme.onSurfaceVariant;
valueColor = null;
} else if (percent <= 15) {
icon = Icons.battery_alert;
iconColor = Colors.orange;
valueColor = Colors.orange;
iconColor = Theme.of(context).colorScheme.tertiary;
valueColor = Theme.of(context).colorScheme.tertiary;
} else {
icon = Icons.battery_full;
iconColor = null;
@@ -316,18 +317,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right),
onTap: () => _editLocation(context, connector),
),
if (connector.currentCustomVars?.containsKey('gps') ?? false) ...[
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.gps_fixed),
title: Text(l10n.settings_locationGPSEnable),
subtitle: Text(l10n.settings_locationGPSEnableSubtitle),
value: connector.currentCustomVars?['gps'] == '1',
onChanged: (value) async {
await connector.setCustomVar(value ? 'gps:1' : 'gps:0');
},
),
],
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.group_add_outlined),
@@ -370,13 +359,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),
leading: Icon(
Icons.delete_outline,
color: Theme.of(context).colorScheme.error,
),
title: Text(l10n.settings_deleteAllPaths),
subtitle: Text(
l10n.settings_deleteAllPathsSubtitle,
style: TextStyle(color: Colors.red[700]),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
onTap: () => connector.deleteAllPaths(),
onTap: () => _confirmDeleteAllPaths(context, connector),
),
const Divider(height: 1),
ListTile(
@@ -394,7 +386,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.restart_alt, color: Colors.orange),
leading: Icon(
Icons.restart_alt,
color: Theme.of(context).colorScheme.tertiary,
),
title: Text(l10n.settings_rebootDevice),
subtitle: Text(l10n.settings_rebootDeviceSubtitle),
onTap: () => _confirmReboot(context, connector),
@@ -662,6 +657,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString();
String? intervalError;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
@@ -697,9 +694,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16),
TextField(
controller: intervalController,
onChanged: (_) {
if (intervalError != null) {
setDialogState(() => intervalError = null);
}
},
decoration: InputDecoration(
labelText: l10n.settings_locationIntervalSec,
border: const OutlineInputBorder(),
errorText: intervalError,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
@@ -730,24 +733,25 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
TextButton(
onPressed: () async {
Navigator.pop(context);
int? interval;
if (hasGPS) {
final intervalText = intervalController.text.trim();
if (intervalText.isEmpty) {
if (intervalText.isNotEmpty) {
interval = int.tryParse(intervalText);
if (interval == null ||
interval < 60 ||
interval >= 86400) {
setDialogState(() {
intervalError = l10n.settings_locationIntervalInvalid;
});
return;
}
final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.settings_locationIntervalInvalid),
);
return;
}
}
Navigator.pop(context);
if (interval != null) {
await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo();
if (!context.mounted) return;
@@ -813,6 +817,36 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
void _confirmDeleteAllPaths(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_deleteAllPaths),
content: Text(l10n.settings_deleteAllPathsSubtitle),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () {
Navigator.pop(context);
connector.deleteAllPaths();
},
child: Text(
l10n.common_deleteAll,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
);
}
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
showDialog(
@@ -832,7 +866,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
child: Text(
l10n.common_reboot,
style: const TextStyle(color: Colors.orange),
style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
),
),
],
@@ -1223,6 +1257,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
bool _clientRepeat = false;
int? _selectedPresetIndex;
_RadioSettingsSnapshot? _lastNonRepeatSnapshot;
String? _frequencyError;
String? _txPowerError;
AppDebugLogService get _appLog =>
Provider.of<AppDebugLogService>(context, listen: false);
@@ -1489,7 +1525,24 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
void _handleManualSettingsChanged(String source) {
_logRadioSettingsState('Manual settings edit: $source');
setState(_syncPresetSelection);
setState(() {
_validateFields();
_syncPresetSelection();
});
}
void _validateFields() {
final l10n = context.l10n;
final freqMHz = double.tryParse(_frequencyController.text);
_frequencyError = (freqMHz == null || freqMHz < 300 || freqMHz > 2500)
? l10n.settings_frequencyInvalid
: null;
final maxTxPower = widget.connector.maxTxPower ?? 22;
final txPower = int.tryParse(_txPowerController.text);
_txPowerError = (txPower == null || txPower < 0 || txPower > maxTxPower)
? '${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'
: null;
}
void _handleClientRepeatChanged(bool enabled) {
@@ -1601,6 +1654,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
content: Text(l10n.settings_error(e.toString())),
);
}
if (!mounted) return;
Navigator.pop(context);
}
@@ -1676,6 +1730,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
labelText: l10n.settings_frequency,
border: const OutlineInputBorder(),
helperText: l10n.settings_frequencyHelper,
errorText: _frequencyError,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
@@ -1759,6 +1814,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
helperText: widget.connector.maxTxPower != null
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
: l10n.settings_txPowerHelper,
errorText: _txPowerError,
),
keyboardType: TextInputType.number,
),
@@ -1780,7 +1836,12 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
FilledButton(
onPressed: (_frequencyError != null || _txPowerError != null)
? null
: _saveSettings,
child: Text(l10n.common_save),
),
],
);
}
+22 -33
View File
@@ -95,12 +95,14 @@ class _TcpScreenState extends State<TcpScreen> {
final isConnecting =
connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.tcp;
final isButtonDisabled =
isConnecting ||
connector.state == MeshCoreConnectionState.scanning;
// A running BLE scan must not block TCP connect: connectTcp() stops
// any active scan before connecting, so the only reason to disable
// the button is a TCP connect already in flight.
final isButtonDisabled = isConnecting;
return Column(
children: [
_buildStatusBar(context, connector),
_buildTransportLinks(context),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -154,41 +156,33 @@ class _TcpScreenState extends State<TcpScreen> {
},
),
),
bottomNavigationBar: SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
);
}
Widget _buildTransportLinks(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Wrap(
spacing: 12,
runSpacing: 8,
children: [
if (PlatformInfo.supportsUsbSerial)
FloatingActionButton.extended(
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'tcp_usb_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12),
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).maybePop();
},
heroTag: 'tcp_ble_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
OutlinedButton.icon(
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
],
),
),
),
);
}
@@ -214,7 +208,7 @@ class _TcpScreenState extends State<TcpScreen> {
statusColor = Colors.orange;
} else {
statusText = l10n.tcpStatus_notConnected;
statusColor = Colors.grey;
statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
}
return Container(
@@ -226,16 +220,11 @@ class _TcpScreenState extends State<TcpScreen> {
Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: statusColor, fontWeight: FontWeight.w500),
),
),
],
@@ -274,7 +263,7 @@ class _TcpScreenState extends State<TcpScreen> {
showDismissibleSnackBar(
context,
content: Text(message),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
+606 -105
View File
@@ -1,21 +1,24 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../models/app_settings.dart';
import '../storage/prefs_manager.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../utils/app_logger.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/routing_sheet.dart';
import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart';
import '../helpers/snack_bar_builder.dart';
import '../widgets/sync_progress_overlay.dart';
import '../widgets/telemetry_location_map.dart';
class TelemetryScreen extends StatefulWidget {
final Contact contact;
@@ -27,6 +30,13 @@ class TelemetryScreen extends StatefulWidget {
}
class _TelemetryScreenState extends State<TelemetryScreen> {
static const int _autoRefreshDefaultIntervalSeconds = 20;
static const int _autoRefreshDefaultQuantity = 10;
static const int _autoRefreshMinIntervalSeconds = 10;
static const int _autoRefreshMaxIntervalSeconds = 300;
static const int _autoRefreshMinQuantity = 1;
static const int _autoRefreshMaxQuantity = 10;
int _tagData = 0;
bool _isLoading = false;
@@ -37,6 +47,17 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedTelemetry;
final TextEditingController _autoRefreshIntervalController =
TextEditingController(text: '$_autoRefreshDefaultIntervalSeconds');
final TextEditingController _autoRefreshQuantityController =
TextEditingController(text: '$_autoRefreshDefaultQuantity');
Timer? _autoRefreshTimer;
bool _isAutoRefreshEnabled = false;
bool _activeTelemetryRequestIsAutoRefresh = false;
bool _autoRefreshLastAttemptFailed = false;
int _autoRefreshCurrentAttempt = 0;
int _autoRefreshTotalAttempts = 0;
int _autoRefreshIntervalSeconds = _autoRefreshDefaultIntervalSeconds;
int _tripTime = 0;
@@ -63,6 +84,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_loadAutoRefreshSettings();
_setupMessageListener();
_loadTelemetry();
_hasData = false;
@@ -82,17 +104,26 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_tagData = reader.readUInt32LE();
_tripTime = reader.readUInt32LE();
_statusTimeout?.cancel();
final isAutoRefreshRequest = _activeTelemetryRequestIsAutoRefresh;
_statusTimeout = Timer(Duration(milliseconds: _tripTime), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
if (isAutoRefreshRequest && _isAutoRefreshEnabled) {
_autoRefreshLastAttemptFailed = true;
}
});
if (!isAutoRefreshRequest) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
if (isAutoRefreshRequest && _isAutoRefreshEnabled) {
_scheduleNextAutoRefreshAttempt();
}
_recordTelemetryResult(false);
});
}
@@ -134,15 +165,21 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
);
}
if (!mounted) return;
final isAutoRefreshRequest = _activeTelemetryRequestIsAutoRefresh;
setState(() {
_parsedTelemetry = parsedTelemetry;
if (isAutoRefreshRequest) {
_autoRefreshLastAttemptFailed = false;
}
_activeTelemetryRequestIsAutoRefresh = false;
});
if (!isAutoRefreshRequest) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_receivedData),
backgroundColor: Colors.green,
);
}
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
@@ -150,14 +187,18 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoaded = true;
_hasData = true;
});
if (isAutoRefreshRequest) {
_scheduleNextAutoRefreshAttempt();
}
}
Future<void> _loadTelemetry() async {
Future<void> _loadTelemetry({bool isAutoRefresh = false}) async {
if (_commandService == null) return;
setState(() {
_isLoading = true;
_isLoaded = false;
_activeTelemetryRequestIsAutoRefresh = isAutoRefresh;
});
try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
@@ -169,7 +210,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
if (widget.contact.type != advTypeChat) {
frame = buildSendBinaryReq(
widget.contact.publicKey,
payload: Uint8List.fromList([reqTypeGetTelemetry]),
payload: buildTelemetryBinaryPayload(),
);
} else {
frame = buildSendTelemetryReq(widget.contact.publicKey);
@@ -180,16 +221,75 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
setState(() {
_isLoading = false;
_isLoaded = false;
if (isAutoRefresh) {
_autoRefreshLastAttemptFailed = true;
}
_activeTelemetryRequestIsAutoRefresh = false;
});
if (isAutoRefresh) {
_scheduleNextAutoRefreshAttempt();
}
if (!isAutoRefresh) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
}
}
}
void _loadAutoRefreshSettings() {
final prefs = PrefsManager.instance;
final contactKey = widget.contact.publicKeyHex;
final interval =
(prefs.getInt(_autoRefreshIntervalKey(contactKey)) ??
_autoRefreshDefaultIntervalSeconds)
.clamp(
_autoRefreshMinIntervalSeconds,
_autoRefreshMaxIntervalSeconds,
)
.toInt();
final quantity =
(prefs.getInt(_autoRefreshQuantityKey(contactKey)) ??
_autoRefreshDefaultQuantity)
.clamp(_autoRefreshMinQuantity, _autoRefreshMaxQuantity)
.toInt();
_autoRefreshIntervalSeconds = interval;
_autoRefreshIntervalController.text = interval.toString();
_autoRefreshQuantityController.text = quantity.toString();
}
Future<void> _saveAutoRefreshSettings() async {
final contactKey = widget.contact.publicKeyHex;
final interval = _clampControllerValue(
controller: _autoRefreshIntervalController,
min: _autoRefreshMinIntervalSeconds,
max: _autoRefreshMaxIntervalSeconds,
fallback: _autoRefreshIntervalSeconds,
);
final quantity = _clampControllerValue(
controller: _autoRefreshQuantityController,
min: _autoRefreshMinQuantity,
max: _autoRefreshMaxQuantity,
fallback: _autoRefreshDefaultQuantity,
);
final prefs = PrefsManager.instance;
await prefs.setInt(_autoRefreshIntervalKey(contactKey), interval);
await prefs.setInt(_autoRefreshQuantityKey(contactKey), quantity);
}
String _autoRefreshIntervalKey(String contactKey) {
return 'telemetry_auto_refresh_interval_$contactKey';
}
String _autoRefreshQuantityKey(String contactKey) {
return 'telemetry_auto_refresh_quantity_$contactKey';
}
void _recordTelemetryResult(bool success) {
final selection = _pendingStatusSelection;
@@ -206,9 +306,13 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
@override
void dispose() {
unawaited(_saveAutoRefreshSettings());
_frameSubscription?.cancel();
_commandService?.dispose();
_statusTimeout?.cancel();
_autoRefreshTimer?.cancel();
_autoRefreshIntervalController.dispose();
_autoRefreshQuantityController.dispose();
super.dispose();
}
@@ -218,7 +322,11 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
final connector = context.watch<MeshCoreConnector>();
final settings = context.watch<AppSettingsService>().settings;
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
final isFloodMode = widget.contact.pathOverride == -1;
final contact = connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
orElse: () => widget.contact,
);
final isFloodMode = contact.pathOverride == -1;
return Scaffold(
appBar: AppBar(
@@ -242,70 +350,11 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
centerTitle: false,
bottom: const SyncProgressAppBarBottom(),
actions: [
PopupMenuButton<String>(
IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(widget.contact, pathLen: -1);
} else {
await connector.setPathOverride(widget.contact, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: widget.contact),
ContactRoutingSheet.show(context, contact: widget.contact),
),
IconButton(
icon: _isLoading
@@ -315,7 +364,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadTelemetry,
onPressed: (_isLoading || _isAutoRefreshEnabled)
? null
: () => _loadTelemetry(),
tooltip: l10n.repeater_refresh,
),
],
@@ -323,7 +374,8 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadTelemetry,
onRefresh: () =>
_isAutoRefreshEnabled ? Future.value() : _loadTelemetry(),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
@@ -333,7 +385,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
Center(
child: Text(
l10n.telemetry_noData,
style: const TextStyle(fontSize: 16, color: Colors.grey),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
if ((_isLoaded || _hasData) &&
@@ -346,6 +401,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
entry['channel'],
isImperialUnits,
),
_buildAutoRefreshCard(),
],
),
),
@@ -359,7 +415,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
int channel,
bool isImperialUnits,
) {
final l10n = context.l10n;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
@@ -384,60 +439,499 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
),
const Divider(),
for (final entry in channelData.entries)
if (entry.key == 'voltage' && channel == 1)
_buildInfoRow(
l10n.telemetry_batteryLabel,
_batteryText(entry.value),
)
else if (entry.key == 'voltage')
_buildInfoRow(
l10n.telemetry_voltageLabel,
l10n.telemetry_voltageValue(entry.value.toString()),
)
else if (entry.key == 'temperature' && channel == 1)
_buildInfoRow(
l10n.telemetry_mcuTemperatureLabel,
_temperatureText(entry.value, isImperialUnits),
)
else if (entry.key == 'temperature')
_buildInfoRow(
l10n.telemetry_temperatureLabel,
_temperatureText(entry.value, isImperialUnits),
)
else if (entry.key == 'current' && channel == 1)
_buildInfoRow(
l10n.telemetry_currentLabel,
l10n.telemetry_currentValue(entry.value.toString()),
)
else
_buildInfoRow(entry.key, entry.value.toString()),
_buildTelemetryField(entry, channel, isImperialUnits),
],
),
),
);
}
Widget _buildTelemetryField(
MapEntry<String, dynamic> entry,
int channel,
bool isImperialUnits,
) {
if (entry.key == 'gps') {
return _buildGpsInfo(entry.value);
}
final display = _formatTelemetryField(
entry.key,
entry.value,
channel,
isImperialUnits,
);
return _buildInfoRow(display.label, display.value);
}
_TelemetryFieldDisplay _formatTelemetryField(
String key,
dynamic value,
int channel,
bool isImperialUnits,
) {
final l10n = context.l10n;
final text = _telemetryValueText(value);
switch (key) {
case 'digitalInput':
return _TelemetryFieldDisplay(l10n.telemetry_digitalInputLabel, text);
case 'digitalOutput':
return _TelemetryFieldDisplay(l10n.telemetry_digitalOutputLabel, text);
case 'analogInput':
return _TelemetryFieldDisplay(
l10n.telemetry_analogInputLabel,
l10n.telemetry_analogValue(text),
);
case 'analogOutput':
return _TelemetryFieldDisplay(
l10n.telemetry_analogOutputLabel,
l10n.telemetry_analogValue(text),
);
case 'generic':
return _TelemetryFieldDisplay(l10n.telemetry_genericLabel, text);
case 'luminosity':
return _TelemetryFieldDisplay(
l10n.telemetry_luminosityLabel,
l10n.telemetry_luminosityValue(text),
);
case 'presence':
return _TelemetryFieldDisplay(l10n.telemetry_presenceLabel, text);
case 'temperature':
return _TelemetryFieldDisplay(
channel == 1
? l10n.telemetry_mcuTemperatureLabel
: l10n.telemetry_temperatureLabel,
_temperatureText(value, isImperialUnits),
);
case 'humidity':
return _TelemetryFieldDisplay(l10n.telemetry_humidityLabel, text);
case 'accelerometer':
return _TelemetryFieldDisplay(
l10n.telemetry_accelerometerLabel,
_telemetryAxisText(value),
);
case 'pressure':
return _TelemetryFieldDisplay(
l10n.telemetry_pressureLabel,
l10n.telemetry_pressureValue(text),
);
case 'altitude':
return _TelemetryFieldDisplay(
l10n.telemetry_altitudeLabel,
l10n.telemetry_altitudeValue(text),
);
case 'voltage':
return _TelemetryFieldDisplay(
channel == 1
? l10n.telemetry_batteryLabel
: l10n.telemetry_voltageLabel,
channel == 1
? _batteryText(value)
: l10n.telemetry_voltageValue(text),
);
case 'current':
return _TelemetryFieldDisplay(
l10n.telemetry_currentLabel,
l10n.telemetry_currentValue(text),
);
case 'frequency':
return _TelemetryFieldDisplay(
l10n.telemetry_frequencyLabel,
l10n.telemetry_frequencyValue(text),
);
case 'percentage':
return _TelemetryFieldDisplay(
l10n.telemetry_percentageLabel,
l10n.telemetry_percentageValue(text),
);
case 'concentration':
return _TelemetryFieldDisplay(
l10n.telemetry_concentrationLabel,
l10n.telemetry_concentrationValue(text),
);
case 'power':
return _TelemetryFieldDisplay(
l10n.telemetry_powerLabel,
l10n.telemetry_powerValue(text),
);
case 'distance':
return _TelemetryFieldDisplay(
l10n.telemetry_distanceLabel,
l10n.telemetry_distanceValue(text),
);
case 'energy':
return _TelemetryFieldDisplay(
l10n.telemetry_energyLabel,
l10n.telemetry_energyValue(text),
);
case 'direction':
return _TelemetryFieldDisplay(
l10n.telemetry_directionLabel,
l10n.telemetry_directionValue(text),
);
case 'time':
return _TelemetryFieldDisplay(
l10n.telemetry_timeLabel,
_telemetryTimeText(value),
);
case 'gyrometer':
return _TelemetryFieldDisplay(
l10n.telemetry_gyrometerLabel,
_telemetryAxisText(value),
);
case 'colour':
return _TelemetryFieldDisplay(
l10n.telemetry_colourLabel,
_telemetryColorText(value),
);
case 'switch':
return _TelemetryFieldDisplay(l10n.telemetry_switchLabel, text);
case 'polyline':
return _TelemetryFieldDisplay(
l10n.telemetry_polylineLabel,
_telemetryMapText(value),
);
default:
return _TelemetryFieldDisplay(key, text);
}
}
Widget _buildAutoRefreshCard() {
final l10n = context.l10n;
final counterText = _autoRefreshCounterText();
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(
Icons.autorenew,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
l10n.common_autoRefresh,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
_buildAutoRefreshNumberField(
controller: _autoRefreshIntervalController,
label: l10n.common_interval,
min: _autoRefreshMinIntervalSeconds,
max: _autoRefreshMaxIntervalSeconds,
fallback: _autoRefreshIntervalSeconds,
),
const SizedBox(height: 12),
_buildAutoRefreshNumberField(
controller: _autoRefreshQuantityController,
label: l10n.telemetry_autoFetchQuantity,
min: _autoRefreshMinQuantity,
max: _autoRefreshMaxQuantity,
fallback: _autoRefreshDefaultQuantity,
),
if (counterText != null) ...[
const SizedBox(height: 12),
Text(
counterText,
textAlign: TextAlign.center,
style: TextStyle(
color: _autoRefreshLastAttemptFailed
? Theme.of(context).colorScheme.error
: null,
fontWeight: FontWeight.w600,
),
),
],
const SizedBox(height: 12),
FilledButton(
onPressed: _isLoading && !_isAutoRefreshEnabled
? null
: _toggleAutoRefresh,
child: _isAutoRefreshEnabled
? SizedBox(
width: double.infinity,
height: 20,
child: Stack(
alignment: Alignment.center,
children: [
Center(child: Text(l10n.common_disable)),
Positioned(
right: 0,
child: SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
)
: Text(l10n.common_enable),
),
],
),
),
);
}
Widget _buildAutoRefreshNumberField({
required TextEditingController controller,
required String label,
required int min,
required int max,
required int fallback,
}) {
return TextField(
controller: controller,
enabled: !_isAutoRefreshEnabled,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
isDense: true,
),
onEditingComplete: () {
_clampControllerValue(
controller: controller,
min: min,
max: max,
fallback: fallback,
);
unawaited(_saveAutoRefreshSettings());
FocusScope.of(context).unfocus();
},
onSubmitted: (_) => unawaited(_saveAutoRefreshSettings()),
onTapOutside: (_) {
unawaited(_saveAutoRefreshSettings());
FocusScope.of(context).unfocus();
},
);
}
String? _autoRefreshCounterText() {
if (!_isAutoRefreshEnabled && _autoRefreshCurrentAttempt == 0) return null;
final counter = '$_autoRefreshCurrentAttempt/$_autoRefreshTotalAttempts';
if (_autoRefreshLastAttemptFailed) {
return '${context.l10n.telemetry_error}: $counter';
}
return counter;
}
void _toggleAutoRefresh() {
if (_isAutoRefreshEnabled) {
_stopAutoRefresh();
return;
}
_startAutoRefresh();
}
void _startAutoRefresh() {
final interval = _clampControllerValue(
controller: _autoRefreshIntervalController,
min: _autoRefreshMinIntervalSeconds,
max: _autoRefreshMaxIntervalSeconds,
fallback: _autoRefreshIntervalSeconds,
);
final quantity = _clampControllerValue(
controller: _autoRefreshQuantityController,
min: _autoRefreshMinQuantity,
max: _autoRefreshMaxQuantity,
fallback: _autoRefreshDefaultQuantity,
);
unawaited(_saveAutoRefreshSettings());
setState(() {
_isAutoRefreshEnabled = true;
_autoRefreshIntervalSeconds = interval;
_autoRefreshTotalAttempts = quantity;
_autoRefreshCurrentAttempt = 0;
_autoRefreshLastAttemptFailed = false;
});
_runAutoRefreshAttempt();
}
void _stopAutoRefresh() {
_autoRefreshTimer?.cancel();
_autoRefreshTimer = null;
if (!mounted) return;
setState(() {
_isAutoRefreshEnabled = false;
});
}
Future<void> _runAutoRefreshAttempt() async {
if (!_isAutoRefreshEnabled || !mounted) return;
if (_autoRefreshCurrentAttempt >= _autoRefreshTotalAttempts) {
_stopAutoRefresh();
return;
}
setState(() {
_autoRefreshCurrentAttempt += 1;
});
await _loadTelemetry(isAutoRefresh: true);
}
void _scheduleNextAutoRefreshAttempt() {
if (!_isAutoRefreshEnabled || !mounted) return;
_autoRefreshTimer?.cancel();
if (_autoRefreshCurrentAttempt >= _autoRefreshTotalAttempts) {
_stopAutoRefresh();
return;
}
// Start the interval only after the current request has finished: after a
// telemetry response, timeout, or send error. This keeps slow replies from
// shortening the intended pause between requests.
_autoRefreshTimer = Timer(
Duration(seconds: _autoRefreshIntervalSeconds),
_runAutoRefreshAttempt,
);
}
int _clampControllerValue({
required TextEditingController controller,
required int min,
required int max,
required int fallback,
}) {
final parsed = int.tryParse(controller.text);
final value = (parsed ?? fallback).clamp(min, max).toInt();
controller.text = value.toString();
controller.selection = TextSelection.collapsed(
offset: controller.text.length,
);
return value;
}
Widget _buildGpsInfo(dynamic value) {
final latitude = _readGpsValue(value, 'latitude');
final longitude = _readGpsValue(value, 'longitude');
final altitude = _readGpsValue(value, 'altitude');
final isValidPosition = _isValidGpsPosition(latitude, longitude);
final gpsText = isValidPosition
? [
latitude!.toStringAsFixed(5),
longitude!.toStringAsFixed(5),
if (altitude != null) '${altitude.toStringAsFixed(1)} m',
].join(', ')
: value.toString();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(context.l10n.telemetry_gpsLabel, gpsText),
if (isValidPosition)
TelemetryLocationMap(
// The map renders only after bounds validation, keeping malformed
// Cayenne payloads from creating an invalid FlutterMap center.
latitude: latitude!,
longitude: longitude!,
label: widget.contact.name,
contactType: widget.contact.type,
contactPublicKeyHex: widget.contact.publicKeyHex,
),
],
);
}
double? _readGpsValue(dynamic value, String key) {
if (value is! Map) return null;
final rawValue = value[key];
if (rawValue is num) return rawValue.toDouble();
return null;
}
bool _isValidGpsPosition(double? latitude, double? longitude) {
if (latitude == null || longitude == null) return false;
const double epsilon = 1e-6;
return (latitude.abs() > epsilon || longitude.abs() > epsilon) &&
latitude >= -90.0 &&
latitude <= 90.0 &&
longitude >= -180.0 &&
longitude <= 180.0;
}
String _telemetryValueText(dynamic value) {
if (value == null) return context.l10n.common_notAvailable;
if (value is double) {
return value.toStringAsFixed(value.truncateToDouble() == value ? 0 : 2);
}
if (value is num) {
return value.toString();
}
return value.toString();
}
String _telemetryAxisText(dynamic value) {
if (value is! Map) return _telemetryValueText(value);
final x = _telemetryValueText(value['x']);
final y = _telemetryValueText(value['y']);
final z = _telemetryValueText(value['z']);
return 'X: $x, Y: $y, Z: $z';
}
String _telemetryColorText(dynamic value) {
if (value is! Map) return _telemetryValueText(value);
final red = _telemetryValueText(value['red']);
final green = _telemetryValueText(value['green']);
final blue = _telemetryValueText(value['blue']);
return 'R: $red, G: $green, B: $blue';
}
String _telemetryMapText(dynamic value) {
if (value is! Map) return _telemetryValueText(value);
return value.entries
.map((entry) => '${entry.key}: ${entry.value}')
.join(', ');
}
String _telemetryTimeText(dynamic value) {
if (value is! num || value <= 0) return _telemetryValueText(value);
final dateTime = DateTime.fromMillisecondsSinceEpoch(
value.toInt() * 1000,
isUtc: true,
).toLocal();
final localizations = MaterialLocalizations.of(context);
final time = localizations.formatTimeOfDay(
TimeOfDay.fromDateTime(dateTime),
);
return '${localizations.formatFullDate(dateTime)} $time';
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
Expanded(
child: Text(
label,
style: TextStyle(
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Text(
const SizedBox(width: 8),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.w400),
),
textAlign: TextAlign.end,
),
],
),
@@ -485,3 +979,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
return '${tempC.toStringAsFixed(1)}°C';
}
}
class _TelemetryFieldDisplay {
final String label;
final String value;
const _TelemetryFieldDisplay(this.label, this.value);
}
+56 -70
View File
@@ -12,7 +12,6 @@ import '../utils/usb_port_labels.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart';
import 'scanner_screen.dart';
import 'tcp_screen.dart';
class UsbScreen extends StatefulWidget {
@@ -100,68 +99,25 @@ class _UsbScreenState extends State<UsbScreen> {
return Column(
children: [
_buildStatusBar(context, connector),
_buildTransportLinks(context),
Expanded(child: _buildPortList(context, connector)),
],
);
},
),
),
bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isLoading = _isLoadingPorts;
final showBle = true;
final showTcp = !PlatformInfo.isWeb;
return SafeArea(
bottomNavigationBar: _supportsHotPlug
? null
: SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showTcp)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const TcpScreen()),
);
},
heroTag: 'usb_tcp_action',
extendedPadding: const EdgeInsets.symmetric(
horizontal: 12,
),
icon: const Icon(Icons.lan),
label: Text(context.l10n.connectionChoiceTcpLabel),
),
if (showTcp && showBle) const SizedBox(width: 12),
if (showBle)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const ScannerScreen(),
),
);
},
heroTag: 'usb_ble_action',
extendedPadding: const EdgeInsets.symmetric(
horizontal: 12,
),
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
if ((showTcp || showBle) && !_supportsHotPlug)
const SizedBox(width: 12),
if (!_supportsHotPlug)
FloatingActionButton.extended(
onPressed: isLoading ? null : _loadPorts,
onPressed: _isLoadingPorts ? null : _loadPorts,
heroTag: 'usb_refresh_action',
extendedPadding: const EdgeInsets.symmetric(
horizontal: 12,
),
icon: isLoading
icon: _isLoadingPorts
? const SizedBox(
width: 20,
height: 20,
@@ -174,7 +130,31 @@ class _UsbScreenState extends State<UsbScreen> {
),
),
);
}
Widget _buildTransportLinks(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Wrap(
spacing: 12,
runSpacing: 8,
children: [
if (!PlatformInfo.isWeb)
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const TcpScreen()),
);
},
icon: const Icon(Icons.lan),
label: Text(context.l10n.connectionChoiceTcpLabel),
),
OutlinedButton.icon(
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
],
),
);
}
@@ -186,7 +166,7 @@ class _UsbScreenState extends State<UsbScreen> {
if (_isLoadingPorts) {
statusText = l10n.usbStatus_searching;
statusColor = Colors.blue;
statusColor = Theme.of(context).colorScheme.primary;
} else if (connector.isUsbTransportConnected) {
switch (connector.state) {
case MeshCoreConnectionState.connected:
@@ -199,7 +179,7 @@ class _UsbScreenState extends State<UsbScreen> {
statusColor = Colors.orange;
default:
statusText = l10n.usbStatus_notConnected;
statusColor = Colors.grey;
statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
}
} else if (connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.usb) {
@@ -207,7 +187,7 @@ class _UsbScreenState extends State<UsbScreen> {
statusColor = Colors.orange;
} else {
statusText = l10n.usbStatus_notConnected;
statusColor = Colors.grey;
statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
}
return Container(
@@ -219,16 +199,11 @@ class _UsbScreenState extends State<UsbScreen> {
Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: statusColor, fontWeight: FontWeight.w500),
),
),
],
@@ -244,11 +219,18 @@ class _UsbScreenState extends State<UsbScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
Icon(
Icons.usb,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
l10n.usbStatus_searching,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
@@ -260,12 +242,19 @@ class _UsbScreenState extends State<UsbScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
Icon(
Icons.usb,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
l10n.usbScreenEmptyState,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
@@ -294,10 +283,7 @@ class _UsbScreenState extends State<UsbScreen> {
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: showRawName ? Text(rawName) : null,
trailing: ElevatedButton(
onPressed: isConnecting ? null : () => _connectPort(port),
child: Text(l10n.common_connect),
),
trailing: const Icon(Icons.chevron_right),
onTap: isConnecting ? null : () => _connectPort(port),
);
},
@@ -387,7 +373,7 @@ class _UsbScreenState extends State<UsbScreen> {
showDismissibleSnackBar(
context,
content: Text(_friendlyErrorMessage(error)),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
+5
View File
@@ -449,6 +449,11 @@ class MessageRetryService extends ChangeNotifier {
});
}
void untrack(String messageId) {
_timeoutTimers[messageId]?.cancel();
_cleanupMessage(messageId);
}
void _cleanupMessage(String messageId) {
_moveAckHashesToHistory(messageId);
_ackHashToMessageId.removeWhere(
+4 -2
View File
@@ -253,7 +253,7 @@ class NotificationService {
await _notifications.show(
id: contactId != null
? 'advert:$contactId'.hashCode
: DateTime.now().millisecondsSinceEpoch,
: DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF,
title: _l10n.notification_newTypeDiscovered(contactType),
body: contactName,
notificationDetails: notificationDetails,
@@ -309,7 +309,9 @@ class NotificationService {
try {
await _notifications.show(
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
id:
channelIndex?.hashCode ??
DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF,
title: channelName,
body: body,
notificationDetails: notificationDetails,
+22 -9
View File
@@ -47,6 +47,7 @@ class TranslationService extends ChangeNotifier {
_langDetectInit = initLangDetect();
}
bool _disposed = false;
bool _isBusy = false;
bool _isDownloading = false;
bool _cancelDownloadRequested = false;
@@ -215,7 +216,7 @@ class TranslationService extends ChangeNotifier {
}
_downloadTotalBytes = totalSize;
notifyListeners();
_notify();
DownloadedModelFile downloaded;
if (supportsRange &&
@@ -268,7 +269,7 @@ class TranslationService extends ChangeNotifier {
throw StateError('Model download failed: HTTP ${response.statusCode}');
}
_downloadTotalBytes ??= response.contentLength;
notifyListeners();
_notify();
final trackedStream = _trackDownloadProgress(response.stream);
return await _fileStore.writeModelBytes(
fileName: fileName,
@@ -313,7 +314,7 @@ class TranslationService extends ChangeNotifier {
throw const TranslationDownloadCancelled();
}
_downloadFileName = 'Merging chunks...';
notifyListeners();
_notify();
combineReached = true;
return await _fileStore.combineChunks(
fileName: fileName,
@@ -361,7 +362,7 @@ class TranslationService extends ChangeNotifier {
}
_cancelDownloadRequested = true;
_lastError = 'Download stopped.';
notifyListeners();
_notify();
}
Future<void> removeModel(TranslationModelRecord model) async {
@@ -469,7 +470,7 @@ class TranslationService extends ChangeNotifier {
} catch (error) {
_lastError = error.toString();
appLogger.warn('Language detection failed: $error');
notifyListeners();
_notify();
return null;
}
}
@@ -538,7 +539,7 @@ class TranslationService extends ChangeNotifier {
} catch (error) {
_lastError = error.toString();
appLogger.warn('Translation request failed: $error');
notifyListeners();
_notify();
return null;
}
}
@@ -631,6 +632,10 @@ class TranslationService extends ChangeNotifier {
final completer = Completer<T>();
_setBusy(true);
_queue = _queue.then((_) async {
if (_disposed) {
completer.completeError(StateError('TranslationService disposed.'));
return;
}
try {
completer.complete(await action());
} catch (error, stackTrace) {
@@ -648,17 +653,24 @@ class TranslationService extends ChangeNotifier {
throw const TranslationDownloadCancelled();
}
_downloadedBytes += chunk.length;
notifyListeners();
_notify();
yield chunk;
}
}
void _notify() {
if (_disposed) {
return;
}
notifyListeners();
}
void _setBusy(bool value) {
if (_isBusy == value) {
return;
}
_isBusy = value;
notifyListeners();
_notify();
}
void _setDownloading(bool value) {
@@ -669,11 +681,12 @@ class TranslationService extends ChangeNotifier {
_downloadTotalBytes = null;
_downloadFileName = null;
}
notifyListeners();
_notify();
}
@override
void dispose() {
_disposed = true;
final engine = _engine;
_engine = null;
_loadedModelPath = null;
+55 -48
View File
@@ -1,34 +1,30 @@
import 'package:flutter/material.dart';
/// MeshCore redesign palette warm field-journal dark theme with
/// phosphor-green signal accents. Mirrors values from the redesign spec.
/// MeshCore palette cool slate dark theme with sky-blue accents.
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);
// Surfaces (cool near-black, slate undertone)
static const bg = Color(0xFF101417);
static const bg1 = Color(0xFF161B1F);
static const bg2 = Color(0xFF1D242A);
static const bg3 = Color(0xFF28313A);
static const bg4 = Color(0xFF344049);
// Lines
static const line = Color(0xFF232C28);
static const line2 = Color(0xFF34403A);
static const line3 = Color(0xFF48564F);
static const line = Color(0xFF222B31);
static const line2 = Color(0xFF344049);
static const line3 = Color(0xFF485762);
// Ink
static const ink = Color(0xFFEFF3E8);
static const ink2 = Color(0xFFBAC4B5);
static const ink3 = Color(0xFF7C8B82);
static const ink4 = Color(0xFF55635B);
static const ink = Color(0xFFE9EEF3);
static const ink2 = Color(0xFFB5C0C9);
static const ink3 = Color(0xFF7C8A95);
static const ink4 = Color(0xFF556470);
// Signal (phosphor)
// Signal-quality green (used only for SNR coloring, not UI chrome)
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);
@@ -41,8 +37,9 @@ class MeshPalette {
static const alertBg = Color(0x1CFF6A5C);
static const alertLine = Color(0x52FF6A5C);
// Blue (dusk sky)
// Blue (sky) primary accent
static const blue = Color(0xFF7FCBF5);
static const blueDim = Color(0xFF4A9CC9);
static const blueBg = Color(0x1C7FCBF5);
static const blueLine = Color(0x477FCBF5);
@@ -51,20 +48,20 @@ class MeshPalette {
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);
// Me bubble (dusk blue)
static const me = Color(0xFF1B2C3D);
static const meBorder = Color(0xFF2C4A66);
static const meInk = Color(0xFFDCE9F5);
// 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);
static const lightBg = Color(0xFFF4F6F8);
static const lightBg1 = Color(0xFFEAEEF2);
static const lightBg2 = Color(0xFFDFE5EA);
static const lightLine = Color(0xFFC3CCD4);
static const lightInk = Color(0xFF10161B);
static const lightInk2 = Color(0xFF3C4853);
static const lightInk3 = Color(0xFF69767F);
static const lightBlue = Color(0xFF2F6EA8);
}
/// Named font stacks Flutter falls back to system fonts when the named
@@ -115,14 +112,18 @@ class 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),
primary: MeshPalette.blue,
onPrimary: Color(0xFF0A1A26),
primaryContainer: MeshPalette.blueBg,
onPrimaryContainer: MeshPalette.blue,
secondary: MeshPalette.magenta,
onSecondary: Color(0xFF201020),
secondaryContainer: Color(0xFF331A33),
onSecondaryContainer: MeshPalette.magenta,
tertiary: MeshPalette.warn,
onTertiary: Color(0xFF1F1206),
tertiaryContainer: Color(0xFF3A2710),
onTertiaryContainer: Color(0xFFFFC58A),
error: MeshPalette.alert,
onError: Color(0xFF1A0A08),
errorContainer: MeshPalette.alertBg,
@@ -141,33 +142,39 @@ class MeshTheme {
scrim: Colors.black54,
inverseSurface: MeshPalette.ink,
onInverseSurface: MeshPalette.bg,
inversePrimary: MeshPalette.signalDim,
inversePrimary: MeshPalette.blueDim,
);
return _build(scheme, Brightness.dark);
}
static ThemeData light() {
const scheme = ColorScheme.light(
primary: MeshPalette.lightSignal,
primary: MeshPalette.lightBlue,
onPrimary: Colors.white,
primaryContainer: Color(0xFFD4E8D8),
onPrimaryContainer: MeshPalette.lightSignal,
secondary: Color(0xFF2F6EA8),
primaryContainer: Color(0xFFD3E4F5),
onPrimaryContainer: Color(0xFF12354F),
secondary: Color(0xFF8C4A8A),
onSecondary: Colors.white,
tertiary: Color(0xFF8C4A8A),
secondaryContainer: Color(0xFFEFD6EE),
onSecondaryContainer: Color(0xFF3D1A3C),
tertiary: Color(0xFF9A5B16),
onTertiary: Colors.white,
tertiaryContainer: Color(0xFFF8E3C9),
onTertiaryContainer: Color(0xFF4A2A05),
error: Color(0xFFB53D2F),
onError: Colors.white,
errorContainer: Color(0xFFF6D9D4),
onErrorContainer: Color(0xFF5C1A12),
surface: MeshPalette.lightBg,
onSurface: MeshPalette.lightInk,
surfaceContainerLowest: MeshPalette.lightBg,
surfaceContainerLow: MeshPalette.lightBg1,
surfaceContainer: MeshPalette.lightBg1,
surfaceContainerHigh: MeshPalette.lightBg2,
surfaceContainerHighest: Color(0xFFD5D0C0),
surfaceContainerHighest: Color(0xFFD2DAE1),
onSurfaceVariant: MeshPalette.lightInk2,
outline: MeshPalette.lightLine,
outlineVariant: Color(0xFFDBD6C6),
outlineVariant: Color(0xFFD8DEE5),
);
return _build(scheme, Brightness.light);
}
+5 -3
View File
@@ -86,7 +86,7 @@ class ByteCountedTextField extends StatelessWidget {
final counterColor = ratio > errorThreshold
? Theme.of(context).colorScheme.error
: ratio > warningThreshold
? Colors.orange
? Theme.of(context).colorScheme.tertiary
: Theme.of(context).colorScheme.onSurfaceVariant;
return Column(
@@ -118,8 +118,9 @@ class ByteCountedTextField extends StatelessWidget {
textInputAction: textInputAction,
onSubmitted: onSubmitted,
),
if (showCounter)
Padding(
Opacity(
opacity: showCounter ? 1 : 0,
child: Padding(
padding: const EdgeInsets.only(top: 4, right: 4),
child: Align(
alignment: Alignment.centerRight,
@@ -129,6 +130,7 @@ class ByteCountedTextField extends StatelessWidget {
),
),
),
),
],
);
},
+16 -6
View File
@@ -6,9 +6,15 @@ import 'signal_ui.dart';
/// A reusable tile widget for displaying a MeshCore device in a list
class DeviceTile extends StatelessWidget {
final ScanResult scanResult;
final VoidCallback onTap;
final VoidCallback? onTap;
final bool isConnecting;
const DeviceTile({super.key, required this.scanResult, required this.onTap});
const DeviceTile({
super.key,
required this.scanResult,
required this.onTap,
this.isConnecting = false,
});
@override
Widget build(BuildContext context) {
@@ -19,16 +25,20 @@ class DeviceTile extends StatelessWidget {
: scanResult.advertisementData.advName;
return ListTile(
enabled: onTap != null || isConnecting,
leading: _buildSignalIcon(rssi),
title: Text(
name.isNotEmpty ? name : context.l10n.common_unknownDevice,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(device.remoteId.toString()),
trailing: ElevatedButton(
onPressed: onTap,
child: Text(context.l10n.common_connect),
),
trailing: isConnecting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
onTap: onTap,
);
}
+7 -3
View File
@@ -17,18 +17,22 @@ class EmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final onSurfaceVariant = Theme.of(context).colorScheme.onSurfaceVariant;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 64, color: Colors.grey[400]),
Icon(icon, size: 64, color: onSurfaceVariant.withValues(alpha: 0.6)),
const SizedBox(height: 16),
Text(title, style: TextStyle(fontSize: 16, color: Colors.grey[600])),
Text(title, style: TextStyle(fontSize: 16, color: onSurfaceVariant)),
if (subtitle != null) ...[
const SizedBox(height: 8),
Text(
subtitle!,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
style: TextStyle(
fontSize: 14,
color: onSurfaceVariant.withValues(alpha: 0.8),
),
textAlign: TextAlign.center,
),
],
+22 -5
View File
@@ -180,7 +180,10 @@ class _GifPickerState extends State<GifPicker> {
const SizedBox(height: 8),
Text(
context.l10n.gifPicker_poweredBy,
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
@@ -197,11 +200,18 @@ class _GifPickerState extends State<GifPicker> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
_error!,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
@@ -219,11 +229,18 @@ class _GifPickerState extends State<GifPicker> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
Icon(
Icons.search_off,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
context.l10n.gifPicker_noGifsFound,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
+128 -9
View File
@@ -1,36 +1,155 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class MessageStatusIcon extends StatelessWidget {
import '../l10n/l10n.dart';
class MessageStatusIcon extends StatefulWidget {
final bool isAcked;
final bool isFailed;
final bool isPending;
final bool isRepeated;
final double size;
/// Base tint for the sent/sending state. On a colored (outgoing) bubble a
/// plain grey tick is nearly invisible, so callers can pass the bubble's own
/// meta/text color for contrast. Falls back to [ColorScheme.onSurfaceVariant].
final Color? onColor;
const MessageStatusIcon({
super.key,
required this.isAcked,
this.isFailed = false,
this.isPending = false,
this.isRepeated = false,
this.size = 14,
this.onColor,
});
@override
State<MessageStatusIcon> createState() => _MessageStatusIconState();
}
class _MessageStatusIconState extends State<MessageStatusIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
if (widget.isPending) _controller.repeat();
}
@override
void didUpdateWidget(MessageStatusIcon oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isPending && !_controller.isAnimating) {
_controller.repeat();
} else if (!widget.isPending && _controller.isAnimating) {
_controller
..stop()
..reset();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (isFailed) {
return Icon(Icons.cancel, size: size, color: Colors.red);
final l10n = context.l10n;
final colorScheme = Theme.of(context).colorScheme;
final double size = widget.size;
final Color baseColor = widget.onColor ?? colorScheme.onSurfaceVariant;
if (widget.isFailed) {
return Semantics(
label: l10n.messageStatus_failed,
child: Icon(Icons.cancel, size: size, color: colorScheme.error),
);
}
final Color color;
if (isAcked) {
color = Colors.green;
} else {
color = Colors.grey;
if (widget.isPending) {
return Semantics(
label: l10n.messageStatus_pending,
child: _SendingDots(
controller: _controller,
color: baseColor,
size: size,
),
);
}
return SvgPicture.asset(
final bool delivered = widget.isAcked || widget.isRepeated;
final String label = widget.isRepeated
? l10n.messageStatus_repeated
: widget.isAcked
? l10n.messageStatus_delivered
: l10n.messageStatus_sent;
final Color color = delivered ? colorScheme.tertiary : baseColor;
return Semantics(
label: label,
child: delivered
? SvgPicture.asset(
'assets/icons/done_all.svg',
width: size,
height: size,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
)
: Icon(Icons.done, size: size, color: color),
);
}
}
/// Three dots that pulse left-to-right while a message is in flight.
class _SendingDots extends StatelessWidget {
final AnimationController controller;
final Color color;
final double size;
const _SendingDots({
required this.controller,
required this.color,
required this.size,
});
@override
Widget build(BuildContext context) {
final double dot = (size * 0.24).clamp(2.0, 4.0);
return SizedBox(
height: size,
child: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: List.generate(3, (i) {
final double phase = (controller.value - i * 0.18) % 1.0;
final double t = phase < 0.5 ? phase * 2 : (1 - phase) * 2;
final double opacity = 0.25 + 0.75 * t.clamp(0.0, 1.0);
return Padding(
padding: EdgeInsets.symmetric(horizontal: dot * 0.28),
child: Container(
width: dot,
height: dot,
decoration: BoxDecoration(
color: color.withValues(alpha: opacity),
shape: BoxShape.circle,
),
),
);
}),
);
},
),
);
}
}
+412
View File
@@ -0,0 +1,412 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/path_helper.dart';
import '../l10n/contact_localization.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
class PathEditorSheet extends StatefulWidget {
final List<Contact> availableContacts;
final List<int> initialPath;
final int pathHashByteWidth;
const PathEditorSheet({
super.key,
required this.availableContacts,
this.initialPath = const [],
this.pathHashByteWidth = pathHashSize,
});
static Future<Uint8List?> show(
BuildContext context, {
required List<Contact> availableContacts,
List<int> initialPath = const [],
int pathHashByteWidth = pathHashSize,
}) {
return showModalBottomSheet<Uint8List>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: FractionallySizedBox(
heightFactor: 0.9,
child: PathEditorSheet(
availableContacts: availableContacts,
initialPath: initialPath,
pathHashByteWidth: pathHashByteWidth,
),
),
),
);
}
@override
State<PathEditorSheet> createState() => _PathEditorSheetState();
}
class _Hop {
final int id;
final Uint8List bytes;
const _Hop(this.id, this.bytes);
}
class _PathEditorSheetState extends State<PathEditorSheet> {
static const int _maxHops = 64;
final List<_Hop> _hops = [];
final _hexController = TextEditingController();
String? _hexError;
bool _syncingHex = false;
String _search = '';
int _nextHopId = 0;
@override
void initState() {
super.initState();
for (final hop in PathHelper.splitPathBytes(
widget.initialPath,
widget.pathHashByteWidth,
)) {
_hops.add(_Hop(_nextHopId++, hop));
}
_syncHexFromHops();
}
@override
void dispose() {
_hexController.dispose();
super.dispose();
}
List<Contact> get _repeaters {
final query = _search.trim().toLowerCase();
return widget.availableContacts
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
.where((c) => c.publicKey.isNotEmpty)
.where((c) => query.isEmpty || c.name.toLowerCase().contains(query))
.toList()
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
}
void _syncHexFromHops() {
_syncingHex = true;
_hexController.text = _hops.map((h) => PathHelper.formatHopHex(h.bytes)).join(',');
_syncingHex = false;
_hexError = null;
}
void _onHexChanged(String text) {
if (_syncingHex) return;
final l10n = context.l10n;
final tokens = text
.split(RegExp(r'[,\s]+'))
.where((t) => t.isNotEmpty)
.toList();
final width = widget.pathHashByteWidth.clamp(1, pubKeySize).toInt();
final expectedChars = width * 2;
final invalid = tokens
.where(
(t) =>
t.length != expectedChars ||
int.tryParse(t, radix: 16) == null,
)
.toList();
setState(() {
if (invalid.isNotEmpty) {
_hexError = l10n.pathEditor_invalidTokens(invalid.join(', '));
return;
}
if (tokens.length > _maxHops) {
_hexError = l10n.pathEditor_tooManyHops;
return;
}
_hexError = null;
_hops
..clear()
..addAll(
tokens.map((t) {
final bytes = <int>[];
for (var i = 0; i < t.length; i += 2) {
bytes.add(int.parse(t.substring(i, i + 2), radix: 16));
}
return _Hop(_nextHopId++, Uint8List.fromList(bytes));
}),
);
});
}
void _addHop(Contact contact) {
if (_hops.length >= _maxHops) return;
final width = widget.pathHashByteWidth.clamp(1, contact.publicKey.length).toInt();
setState(() {
_hops.add(
_Hop(_nextHopId++, Uint8List.fromList(contact.publicKey.sublist(0, width))),
);
_syncHexFromHops();
});
}
void _removeHop(int index) {
setState(() {
_hops.removeAt(index);
_syncHexFromHops();
});
}
void _reorderHop(int oldIndex, int newIndex) {
setState(() {
final hop = _hops.removeAt(oldIndex);
_hops.insert(newIndex, hop);
_syncHexFromHops();
});
}
void _save() {
final bytes = <int>[];
for (final hop in _hops) {
bytes.addAll(hop.bytes);
}
Navigator.pop(
context,
Uint8List.fromList(bytes),
);
}
Widget _hopTile(BuildContext context, int index) {
final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme;
final hop = _hops[index];
final hex = PathHelper.formatHopHex(hop.bytes);
final matches = widget.availableContacts.where((contact) {
if (contact.publicKey.length < hop.bytes.length) return false;
return contact.publicKey
.sublist(0, hop.bytes.length)
.asMap()
.entries
.every((entry) => entry.value == hop.bytes[entry.key]);
}).toList();
final name = matches.isEmpty
? null
: matches.length == 1
? matches.first.name
: matches.map((c) => c.name).join(' | ');
return ListTile(
key: ValueKey(hop.id),
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 14,
backgroundColor: scheme.primaryContainer,
child: Text(
'${index + 1}',
style: TextStyle(fontSize: 12, color: scheme.onPrimaryContainer),
),
),
title: Text(
name ?? l10n.pathEditor_unknownHop,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(hex),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
tooltip: l10n.pathEditor_removeHop,
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
onPressed: () => _removeHop(index),
),
ReorderableDragStartListener(
index: index,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 12),
child: Icon(Icons.drag_handle),
),
),
],
),
);
}
Widget _repeaterTile(BuildContext context, Contact contact) {
final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme;
final isRepeater = contact.type == advTypeRepeater;
final full = _hops.length >= _maxHops;
return ListTile(
contentPadding: EdgeInsets.zero,
enabled: !full,
leading: CircleAvatar(
radius: 16,
backgroundColor: isRepeater
? scheme.primaryContainer
: scheme.secondaryContainer,
child: Icon(
isRepeater ? Icons.router : Icons.meeting_room,
size: 16,
color: isRepeater
? scheme.onPrimaryContainer
: scheme.onSecondaryContainer,
),
),
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(
'${contact.typeLabel(l10n)}${PathHelper.formatHopHex(contact.publicKey.sublist(0, widget.pathHashByteWidth.clamp(1, contact.publicKey.length).toInt()))}',
),
trailing: const Icon(Icons.add_circle_outline),
onTap: full ? null : () => _addHop(contact),
);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final repeaters = _repeaters;
return Column(
children: [
const SizedBox(height: 8),
Container(
width: 32,
height: 4,
decoration: BoxDecoration(
color: scheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.pathEditor_title, style: theme.textTheme.titleLarge),
Text(
l10n.pathEditor_hopCounter(_hops.length),
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
children: [
if (_hops.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
l10n.pathEditor_noHops,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
)
else
ReorderableListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
buildDefaultDragHandles: false,
itemCount: _hops.length,
onReorderItem: _reorderHop,
itemBuilder: _hopTile,
),
const Divider(),
const SizedBox(height: 8),
Text(l10n.pathEditor_addHops, style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
TextField(
onChanged: (value) => setState(() => _search = value),
decoration: InputDecoration(
labelText: l10n.pathEditor_searchRepeaters,
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
isDense: true,
),
),
const SizedBox(height: 4),
if (repeaters.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
l10n.path_noRepeatersFound,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
)
else
...repeaters.map((c) => _repeaterTile(context, c)),
ExpansionTile(
tilePadding: EdgeInsets.zero,
title: Text(
l10n.pathEditor_advancedHex,
style: theme.textTheme.titleSmall,
),
children: [
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: _hexController,
onChanged: _onHexChanged,
textCapitalization: TextCapitalization.characters,
decoration: InputDecoration(
labelText: l10n.pathEditor_hexLabel,
helperText: _hexError == null
? l10n.pathEditor_hexHelper
: null,
errorText: _hexError,
border: const OutlineInputBorder(),
),
),
),
],
),
],
),
),
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
children: [
Expanded(
child: TextButton(
style: TextButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
onPressed: _hexError != null ? null : _save,
child: Text(l10n.pathEditor_usePath),
),
),
],
),
),
),
],
);
}
}
-524
View File
@@ -1,524 +0,0 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/models/path_history.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
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';
import 'path_selection_dialog.dart';
class PathManagementDialog {
static Future<void> show(BuildContext context, {required Contact contact}) {
return showDialog<void>(
context: context,
builder: (context) => _PathManagementDialog(contact: contact),
);
}
}
class _PathManagementDialog extends StatefulWidget {
final Contact contact;
const _PathManagementDialog({required this.contact});
@override
State<_PathManagementDialog> createState() => _PathManagementDialogState();
}
class _PathManagementDialogState extends State<_PathManagementDialog> {
bool _showAllPaths = false;
int _resolveContactIndex = -1;
Contact _resolveContact(MeshCoreConnector connector) {
if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
);
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
}
String _formatRelativeTime(BuildContext context, DateTime? time) {
if (time == null) return '';
final l10n = context.l10n;
final diff = DateTime.now().difference(time);
if (diff.inSeconds < 60) return l10n.time_justNow;
if (diff.inMinutes < 60) return l10n.time_minutesAgo(diff.inMinutes);
if (diff.inHours < 24) return l10n.time_hoursAgo(diff.inHours);
return l10n.time_daysAgo(diff.inDays);
}
void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
final l10n = context.l10n;
if (pathBytes.isEmpty) {
showDismissibleSnackBar(
context,
content: Text(l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2),
);
return;
}
final connector = context.read<MeshCoreConnector>();
final allContacts = connector.allContacts;
final formattedPath = PathHelper.splitPathBytes(
pathBytes,
connector.pathHashByteWidth,
).map(PathHelper.formatHopHex).join(',');
final resolvedNames = PathHelper.resolvePathNames(
pathBytes,
allContacts,
connector.pathHashByteWidth,
);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.chat_fullPath),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(formattedPath),
const SizedBox(height: 8),
SelectableText(
resolvedNames,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes),
flipPathAround: true,
targetContact: widget.contact,
pathHashByteWidth: connector.pathHashByteWidth,
),
),
),
child: Text(context.l10n.contacts_pathTrace),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_close),
),
],
),
);
}
Future<void> _setCustomPath(
BuildContext context,
MeshCoreConnector connector,
Contact currentContact,
) async {
final l10n = context.l10n;
if (currentContact.pathLength > 0 &&
currentContact.path.isEmpty &&
connector.isConnected) {
connector.getContacts();
}
final pathForInput = currentContact.pathFormattedIdList(
connector.pathHashByteWidth,
);
final availableContacts = connector.allContacts
.where((c) => c.publicKeyHex != currentContact.publicKeyHex)
.toList();
final result = await PathSelectionDialog.show(
context,
availableContacts: availableContacts,
initialPath: pathForInput.isEmpty ? null : pathForInput,
currentPathLabel: currentContact.pathLabel(l10n),
onRefresh: connector.isConnected ? connector.getContacts : null,
pathHashByteWidth: connector.pathHashByteWidth,
);
if (result != null && context.mounted) {
final hopsCount = result.length ~/ connector.pathHashByteWidth;
await connector.setPathOverride(
currentContact,
pathLen: hopsCount,
pathBytes: result,
);
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.chat_hopsCount(hopsCount)),
duration: const Duration(seconds: 2),
);
}
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Consumer2<MeshCoreConnector, PathHistoryService>(
builder: (context, connector, pathService, _) {
final currentContact = _resolveContact(connector);
final paths = pathService.getRecentPaths(currentContact.publicKeyHex);
final repeatersList = List.of(connector.directRepeaters)
..sort((a, b) => b.ranking.compareTo(a.ranking));
if (repeatersList.isEmpty) {
_showAllPaths = true;
}
final directRepeater = repeatersList.isEmpty
? null
: repeatersList.first;
final secondDirectRepeater = repeatersList.length < 2
? null
: repeatersList.elementAt(1);
final thirdDirectRepeater = repeatersList.length < 3
? null
: repeatersList.elementAt(2);
List<MapEntry<int, MapEntry<Color, PathRecord>>> pathsWithRepeaters =
paths.map((path) {
final isDirectRepeater =
directRepeater != null &&
directRepeater.matchesPathStart(
path.pathBytes,
connector.pathHashByteWidth,
);
final isSecondDirectRepeater =
secondDirectRepeater != null &&
secondDirectRepeater.matchesPathStart(
path.pathBytes,
connector.pathHashByteWidth,
);
final isThirdDirectRepeater =
thirdDirectRepeater != null &&
thirdDirectRepeater.matchesPathStart(
path.pathBytes,
connector.pathHashByteWidth,
);
int ranking = -1;
Color color = Colors.grey;
if (isDirectRepeater) {
color = Colors.green;
ranking = 3;
} else if (isSecondDirectRepeater) {
color = Colors.yellow;
ranking = 2;
} else if (isThirdDirectRepeater) {
color = Colors.red;
ranking = 1;
} else if (path.wasFloodDiscovery) {
color = Colors.blue;
ranking = 0;
}
return MapEntry(ranking, MapEntry(color, path));
}).toList();
pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key));
return AlertDialog(
title: Text(l10n.chat_pathManagement),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.path_currentPath(currentContact.pathLabel(l10n)),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 12),
if (paths.isNotEmpty) ...[
if (repeatersList.isNotEmpty)
FeatureToggleRow(
title: l10n.chat_ShowAllPaths,
subtitle: "",
value: _showAllPaths,
onChanged: (val) {
setState(() {
_showAllPaths = val;
});
},
),
Text(
l10n.chat_recentAckPaths,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
if (pathsWithRepeaters.length >= 100) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.amberAccent,
borderRadius: BorderRadius.circular(8),
),
child: Text(
l10n.chat_pathHistoryFull,
style: const TextStyle(fontSize: 12),
),
),
],
const SizedBox(height: 8),
...pathsWithRepeaters.map((entry) {
final path = entry.value.value;
final color = entry.value.key;
if (!_showAllPaths && entry.key < 1) {
return const SizedBox.shrink();
} else {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
dense: true,
leading: CircleAvatar(
radius: 16,
backgroundColor: color,
child: Text(
path.routeWeight.toStringAsFixed(1),
style: const TextStyle(fontSize: 10),
),
),
title: Text(
l10n.chat_hopsCount(path.hopCount),
style: const TextStyle(fontSize: 14),
),
isThreeLine: true,
subtitle: Text(
'${(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(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close, size: 16),
tooltip: l10n.chat_removePath,
onPressed: () async {
await pathService.removePathRecord(
currentContact.publicKeyHex,
path.pathBytes,
);
},
),
path.wasFloodDiscovery
? const Icon(
Icons.waves,
size: 16,
color: Colors.grey,
)
: const Icon(
Icons.route,
size: 16,
color: Colors.grey,
),
],
),
onLongPress: () =>
_showFullPathDialog(context, path.pathBytes),
onTap: () async {
if (path.pathBytes.isEmpty) {
showDismissibleSnackBar(
context,
content: Text(
l10n.chat_pathDetailsNotAvailable,
),
duration: const Duration(seconds: 2),
);
return;
}
final pathBytes = Uint8List.fromList(
path.pathBytes,
);
await connector.setPathOverride(
currentContact,
pathLen: path.hopCount,
pathBytes: pathBytes,
);
if (!context.mounted) return;
Navigator.pop(context);
showDismissibleSnackBar(
context,
content: Text(
l10n.path_usingHopsPath(path.hopCount),
),
duration: const Duration(seconds: 2),
);
},
),
);
}
}),
const Divider(),
] else ...[
Text(l10n.chat_noPathHistoryYet),
const Divider(),
],
// Flood delivery stats
Builder(
builder: (context) {
final floodStats = pathService.getFloodStats(
currentContact.publicKeyHex,
);
if (floodStats == null ||
(floodStats.successCount == 0 &&
floodStats.failureCount == 0)) {
return const SizedBox.shrink();
}
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
dense: true,
leading: const CircleAvatar(
radius: 16,
backgroundColor: Colors.blue,
child: Icon(Icons.waves, size: 16),
),
title: const Text(
'Flood Mode',
style: TextStyle(fontSize: 14),
),
subtitle: Text(
'${floodStats.successCount} ${l10n.chat_successes} / ${floodStats.failureCount} failures'
'${floodStats.lastTripTimeMs > 0 ? '${(floodStats.lastTripTimeMs / 1000).toStringAsFixed(2)}s' : ''}'
'${floodStats.lastUsed != null ? '${_formatRelativeTime(context, floodStats.lastUsed!)}' : ''}',
style: const TextStyle(fontSize: 11),
),
),
);
},
),
const SizedBox(height: 8),
Text(
l10n.chat_pathActions,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
const SizedBox(height: 8),
ListTile(
dense: true,
leading: const CircleAvatar(
radius: 16,
backgroundColor: Colors.purple,
child: Icon(Icons.edit_road, size: 16),
),
title: Text(
l10n.chat_setCustomPath,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.chat_setCustomPathSubtitle,
style: const TextStyle(fontSize: 11),
),
onTap: () async {
await _setCustomPath(context, connector, currentContact);
},
),
ListTile(
dense: true,
leading: const CircleAvatar(
radius: 16,
backgroundColor: Colors.orange,
child: Icon(Icons.clear_all, size: 16),
),
title: Text(
l10n.chat_clearPath,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.chat_clearPathSubtitle,
style: const TextStyle(fontSize: 11),
),
onTap: () async {
await connector.clearContactPath(currentContact);
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.chat_pathCleared),
duration: const Duration(seconds: 2),
);
Navigator.pop(context);
},
),
ListTile(
dense: true,
leading: const CircleAvatar(
radius: 16,
backgroundColor: Colors.blue,
child: Icon(Icons.waves, size: 16),
),
title: Text(
l10n.chat_forceFloodMode,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.chat_floodModeSubtitle,
style: const TextStyle(fontSize: 11),
),
onTap: () async {
await connector.setPathOverride(
currentContact,
pathLen: -1,
);
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.chat_floodModeEnabled),
duration: const Duration(seconds: 2),
);
Navigator.pop(context);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_close),
),
],
);
},
);
}
}
-359
View File
@@ -1,359 +0,0 @@
import 'dart:typed_data';
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 {
final List<Contact> availableContacts;
final String title;
final String? initialPath;
final String? currentPathLabel;
final VoidCallback? onRefresh;
final int pathHashByteWidth;
const PathSelectionDialog({
super.key,
required this.availableContacts,
required this.title,
this.initialPath,
this.currentPathLabel,
this.onRefresh,
this.pathHashByteWidth = 1,
});
@override
State<PathSelectionDialog> createState() => _PathSelectionDialogState();
static Future<Uint8List?> show(
BuildContext context, {
required List<Contact> availableContacts,
String? title,
String? initialPath,
String? currentPathLabel,
VoidCallback? onRefresh,
int pathHashByteWidth = 1,
}) {
return showDialog<Uint8List?>(
context: context,
builder: (context) => PathSelectionDialog(
availableContacts: availableContacts,
title: title ?? context.l10n.path_enterCustomPath,
initialPath: initialPath,
currentPathLabel: currentPathLabel,
onRefresh: onRefresh,
pathHashByteWidth: pathHashByteWidth,
),
);
}
}
class _PathSelectionDialogState extends State<PathSelectionDialog> {
late TextEditingController _controller;
final List<Contact> _selectedContacts = [];
List<Contact> _validContacts = [];
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialPath ?? '');
_filterValidContacts();
}
@override
void didUpdateWidget(PathSelectionDialog oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.availableContacts != oldWidget.availableContacts) {
_filterValidContacts();
}
}
void _filterValidContacts() {
_validContacts = widget.availableContacts
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
.toList();
}
void _updateTextFromContacts() {
final width = widget.pathHashByteWidth.clamp(1, 4);
final hexCharsPerHop = width * 2;
final pathParts = _selectedContacts
.map((contact) {
if (contact.publicKeyHex.length >= hexCharsPerHop) {
return contact.publicKeyHex.substring(0, hexCharsPerHop);
}
return '';
})
.where((s) => s.isNotEmpty)
.toList();
_controller.text = pathParts.join(',');
}
void _toggleContact(Contact contact) {
setState(() {
if (_selectedContacts.contains(contact)) {
_selectedContacts.remove(contact);
} else {
_selectedContacts.add(contact);
}
_updateTextFromContacts();
});
}
void _clearSelection() {
setState(() {
_selectedContacts.clear();
_controller.clear();
});
}
Future<void> _validateAndSubmit() async {
final l10n = context.l10n;
final path = _controller.text.trim().toUpperCase();
if (path.isEmpty) {
if (mounted) Navigator.pop(context);
return;
}
// Parse comma-separated hex prefixes
final pathIds = path
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
final pathBytesList = <int>[];
final invalidPrefixes = <String>[];
final pathHashByteWidth = widget.pathHashByteWidth.clamp(1, 4).toInt();
final hexCharsPerHop = pathHashByteWidth * 2;
for (final id in pathIds) {
if (id.length < hexCharsPerHop) {
invalidPrefixes.add(id);
continue;
}
final prefix = id.substring(0, hexCharsPerHop);
try {
for (int i = 0; i < prefix.length; i += 2) {
pathBytesList.add(int.parse(prefix.substring(i, i + 2), radix: 16));
}
} catch (e) {
invalidPrefixes.add(id);
}
}
if (!mounted) return;
// Show error for invalid prefixes
if (invalidPrefixes.isNotEmpty) {
showDismissibleSnackBar(
context,
content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))),
duration: const Duration(seconds: 3),
backgroundColor: Colors.red,
);
return;
}
final hopCount = pathBytesList.length ~/ pathHashByteWidth;
final maxHopCountByBytes = maxPathSize ~/ pathHashByteWidth;
final maxHopCount = maxHopCountByBytes < 0x3F ? maxHopCountByBytes : 0x3F;
// path_len stores hop count in 6 bits and the path buffer is byte-limited.
if (hopCount > maxHopCount || pathBytesList.length > maxPathSize) {
showDismissibleSnackBar(
context,
content: Text(l10n.path_tooLong),
duration: const Duration(seconds: 3),
backgroundColor: Colors.red,
);
return;
}
if (pathBytesList.isNotEmpty && mounted) {
Navigator.pop(context, Uint8List.fromList(pathBytesList));
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return AlertDialog(
title: Text(widget.title),
content: SingleChildScrollView(
child: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.currentPathLabel != null) ...[
Row(
children: [
Text(
l10n.path_currentPathLabel,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (widget.onRefresh != null)
TextButton.icon(
onPressed: widget.onRefresh,
icon: const Icon(Icons.refresh, size: 16),
label: Text(l10n.common_reload),
),
],
),
Text(
widget.currentPathLabel!,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
const SizedBox(height: 16),
],
Text(
l10n.path_hexPrefixInstructions,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 8),
Text(
l10n.path_hexPrefixExample,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
const SizedBox(height: 16),
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: l10n.path_labelHexPrefixes,
hintText: l10n.path_hexPrefixExample,
border: const OutlineInputBorder(),
helperText: l10n.path_helperMaxHops,
),
textCapitalization: TextCapitalization.characters,
maxLength: 188, // 63 one-byte hops * 2 chars + 62 commas
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
Row(
children: [
Text(
l10n.path_selectFromContacts,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (_selectedContacts.isNotEmpty)
TextButton(
onPressed: _clearSelection,
child: Text(l10n.common_clear),
),
],
),
const SizedBox(height: 8),
if (_validContacts.isEmpty) ...[
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Icon(
Icons.info_outline,
size: 48,
color: Colors.grey,
),
const SizedBox(height: 16),
Text(
l10n.path_noRepeatersFound,
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.path_customPathsRequire,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
],
),
),
),
] else ...[
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: ListView.builder(
shrinkWrap: true,
itemCount: _validContacts.length,
itemBuilder: (context, index) {
final contact = _validContacts[index];
final isSelected = _selectedContacts.contains(contact);
return ListTile(
dense: true,
leading: CircleAvatar(
radius: 16,
backgroundColor: isSelected
? Colors.green
: (contact.type == 2
? Colors.blue
: Colors.purple),
child: Icon(
contact.type == 2
? Icons.router
: Icons.meeting_room,
size: 16,
color: Colors.white,
),
),
title: Text(
contact.name,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
'${contact.typeLabel(l10n)}${contact.publicKeyHex.substring(0, widget.pathHashByteWidth.clamp(1, 4) * 2)}',
style: const TextStyle(fontSize: 10),
),
trailing: isSelected
? const Icon(
Icons.check_circle,
color: Colors.green,
)
: const Icon(Icons.add_circle_outline),
onTap: () => _toggleContact(contact),
);
},
),
),
],
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: _validateAndSubmit,
child: Text(l10n.path_setPath),
),
],
);
}
}
+7 -20
View File
@@ -67,10 +67,12 @@ class QuickSwitchBar extends StatelessWidget {
destinations: [
NavigationDestination(
icon: _buildIconWithBadge(
context,
const Icon(Icons.people_outline),
contactsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
context,
const Icon(Icons.people),
contactsUnreadCount,
),
@@ -78,10 +80,12 @@ class QuickSwitchBar extends StatelessWidget {
),
NavigationDestination(
icon: _buildIconWithBadge(
context,
const Icon(Icons.tag),
channelsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
context,
const Icon(Icons.tag),
channelsUnreadCount,
),
@@ -101,26 +105,9 @@ class QuickSwitchBar extends StatelessWidget {
);
}
Widget _buildIconWithBadge(Icon icon, int count) {
Widget _buildIconWithBadge(BuildContext context, Icon icon, int count) {
if (count <= 0) return icon;
return Stack(
clipBehavior: Clip.none,
children: [
icon,
Positioned(
right: -2,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.redAccent,
shape: BoxShape.circle,
),
),
),
],
);
final label = count > 99 ? '99+' : '$count';
return Badge(label: Text(label), child: icon);
}
}
+5 -1
View File
@@ -59,12 +59,16 @@ class _RadioStatsIconButtonState extends State<RadioStatsIconButton> {
active: connector.radioStatsAirActivityPulse,
);
if (widget.compact) {
return GestureDetector(
return Semantics(
label: context.l10n.radioStats_tooltip,
button: true,
child: GestureDetector(
onTap: () => pushCompanionRadioStatsScreen(context),
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: dot,
),
),
);
}
return Tooltip(
+11 -11
View File
@@ -1,5 +1,4 @@
import 'dart:async';
import '../utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
@@ -11,7 +10,7 @@ import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart';
import 'path_management_dialog.dart';
import 'routing_sheet.dart';
class RepeaterLoginDialog extends StatefulWidget {
final Contact repeater;
@@ -276,7 +275,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
return AlertDialog(
title: Row(
children: [
const Icon(Icons.cell_tower, color: Colors.orange),
Icon(Icons.cell_tower, color: Theme.of(context).colorScheme.tertiary),
const SizedBox(width: 8),
Expanded(
child: Column(
@@ -288,7 +287,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
@@ -365,9 +364,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
}
},
onSubmitted: (_) => _handleLogin(),
autofocus:
!PlatformInfo.isMobile &&
_passwordController.text.isEmpty,
autofocus: _passwordController.text.isEmpty,
),
const SizedBox(height: 12),
CheckboxListTile(
@@ -469,14 +466,17 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
const SizedBox(height: 4),
Text(
repeater.pathLabel(context.l10n),
style: const TextStyle(fontSize: 11, color: Colors.grey),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
ContactRoutingSheet.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths),
),
@@ -497,12 +497,12 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
color: Theme.of(context).colorScheme.onPrimary,
),
),
const SizedBox(width: 12),
+11 -8
View File
@@ -12,7 +12,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart';
import '../helpers/snack_bar_builder.dart';
import 'path_management_dialog.dart';
import 'routing_sheet.dart';
class RoomLoginDialog extends StatefulWidget {
final Contact room;
@@ -181,7 +181,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
showDismissibleSnackBar(
context,
content: Text(context.l10n.login_failed(e.toString())),
backgroundColor: Colors.red,
backgroundColor: Theme.of(context).colorScheme.error,
);
}
}
@@ -232,7 +232,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
return AlertDialog(
title: Row(
children: [
const Icon(Icons.group, color: Colors.purple),
Icon(Icons.group, color: Theme.of(context).colorScheme.secondary),
const SizedBox(width: 8),
Expanded(
child: Column(
@@ -244,7 +244,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Colors.grey[600],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
@@ -395,14 +395,17 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
const SizedBox(height: 4),
Text(
repeater.pathLabel(context.l10n),
style: const TextStyle(fontSize: 11, color: Colors.grey),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
ContactRoutingSheet.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths),
),
@@ -423,12 +426,12 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
color: Theme.of(context).colorScheme.onPrimary,
),
),
const SizedBox(width: 12),
+751
View File
@@ -0,0 +1,751 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../helpers/path_helper.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_history.dart';
import '../screens/path_trace_map.dart';
import '../services/path_history_service.dart';
import 'path_editor_sheet.dart';
enum _RoutingMode { auto, flood, manual }
enum _PathQuality { strong, good, fair, proven, flood, untested }
class ContactRoutingSheet {
static Future<void> show(BuildContext context, {required Contact contact}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => DraggableScrollableSheet(
expand: false,
initialChildSize: 0.75,
minChildSize: 0.4,
maxChildSize: 0.95,
builder: (context, scrollController) => _RoutingSheetBody(
contact: contact,
scrollController: scrollController,
),
),
);
}
}
class _RoutingSheetBody extends StatefulWidget {
final Contact contact;
final ScrollController scrollController;
const _RoutingSheetBody({
required this.contact,
required this.scrollController,
});
@override
State<_RoutingSheetBody> createState() => _RoutingSheetBodyState();
}
class _RoutingSheetBodyState extends State<_RoutingSheetBody> {
int _resolveContactIndex = -1;
String? _syncStatus;
Contact _resolveContact(MeshCoreConnector connector) {
if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
);
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
}
_RoutingMode _modeOf(Contact contact) {
final override = contact.pathOverride;
if (override == null) return _RoutingMode.auto;
return override < 0 ? _RoutingMode.flood : _RoutingMode.manual;
}
Future<void> _selectMode(
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
) async {
switch (mode) {
case _RoutingMode.auto:
setState(() => _syncStatus = null);
await connector.setPathOverride(contact, pathLen: null);
case _RoutingMode.flood:
setState(() => _syncStatus = null);
await connector.setPathOverride(contact, pathLen: -1);
case _RoutingMode.manual:
await _editManualPath(connector, contact);
}
}
Future<void> _editManualPath(
MeshCoreConnector connector,
Contact contact,
) async {
final override = contact.pathOverride;
final initial = override != null && override > 0
? (contact.pathOverrideBytes ?? Uint8List(0))
: (contact.pathLength > 0 ? contact.path : Uint8List(0));
final available = connector.allContacts
.where((c) => c.publicKeyHex != contact.publicKeyHex)
.toList();
final result = await PathEditorSheet.show(
context,
availableContacts: available,
initialPath: initial,
pathHashByteWidth: connector.pathHashByteWidth,
);
if (result == null || !mounted) return;
final hopCount = result.length ~/ connector.pathHashByteWidth;
await connector.setPathOverride(
contact,
pathLen: hopCount,
pathBytes: result,
);
await _verifyPath(connector, contact, result);
}
Future<void> _applyHistoryPath(
MeshCoreConnector connector,
Contact contact,
PathRecord record,
) async {
final bytes = Uint8List.fromList(record.pathBytes);
final hopCount = bytes.length ~/ connector.pathHashByteWidth;
await connector.setPathOverride(
contact,
pathLen: hopCount,
pathBytes: bytes,
);
await _verifyPath(connector, contact, bytes);
}
Future<void> _verifyPath(
MeshCoreConnector connector,
Contact contact,
Uint8List bytes,
) async {
if (!mounted) return;
if (!connector.isConnected) {
setState(() => _syncStatus = context.l10n.chat_pathSavedLocally);
return;
}
setState(() => _syncStatus = null);
final verified = await connector.verifyContactPathOnDevice(contact, bytes);
if (!mounted) return;
setState(
() => _syncStatus = verified
? context.l10n.chat_pathDeviceConfirmed
: context.l10n.chat_pathDeviceNotConfirmed,
);
}
Future<void> _forgetPath(MeshCoreConnector connector, Contact contact) async {
await connector.clearContactPath(contact);
if (!mounted) return;
setState(() => _syncStatus = context.l10n.chat_pathCleared);
}
_PathQuality _qualityOf(
MeshCoreConnector connector,
PathRecord record,
List<DirectRepeater> ranked,
) {
if (record.pathBytes.isNotEmpty) {
for (var i = 0; i < ranked.length && i < 3; i++) {
if (ranked[i].matchesPathStart(
record.pathBytes,
connector.pathHashByteWidth,
)) {
return switch (i) {
0 => _PathQuality.strong,
1 => _PathQuality.good,
_ => _PathQuality.fair,
};
}
}
}
if (record.successCount > 0) return _PathQuality.proven;
if (record.wasFloodDiscovery) return _PathQuality.flood;
return _PathQuality.untested;
}
String _qualityLabel(BuildContext context, _PathQuality quality) {
final l10n = context.l10n;
return switch (quality) {
_PathQuality.strong => l10n.routing_qualityStrong,
_PathQuality.good => l10n.routing_qualityGood,
_PathQuality.fair => l10n.routing_qualityFair,
_PathQuality.proven => l10n.routing_qualityWorked,
_PathQuality.flood => l10n.routing_qualityFlood,
_PathQuality.untested => l10n.routing_qualityUntested,
};
}
IconData _qualityIcon(_PathQuality quality) {
return switch (quality) {
_PathQuality.strong => Icons.signal_cellular_alt,
_PathQuality.good => Icons.signal_cellular_alt_2_bar,
_PathQuality.fair => Icons.signal_cellular_alt_1_bar,
_PathQuality.proven => Icons.check_circle_outline,
_PathQuality.flood => Icons.waves,
_PathQuality.untested => Icons.route,
};
}
String _relativeTime(BuildContext context, DateTime time) {
final l10n = context.l10n;
final diff = DateTime.now().difference(time);
if (diff.inSeconds < 60) return l10n.time_justNow;
if (diff.inMinutes < 60) return l10n.time_minutesAgo(diff.inMinutes);
if (diff.inHours < 24) return l10n.time_hoursAgo(diff.inHours);
return l10n.time_daysAgo(diff.inDays);
}
String _modeHint(BuildContext context, _RoutingMode mode) {
final l10n = context.l10n;
return switch (mode) {
_RoutingMode.auto => l10n.routing_modeAutoHint,
_RoutingMode.flood => l10n.routing_modeFloodHint,
_RoutingMode.manual => l10n.routing_modeManualHint,
};
}
String _routeText(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
) {
final l10n = context.l10n;
switch (mode) {
case _RoutingMode.flood:
return l10n.routing_floodBroadcast;
case _RoutingMode.manual:
final bytes = contact.pathOverrideBytes ?? Uint8List(0);
if (bytes.isEmpty) return l10n.routing_directNoHops;
return PathHelper.resolvePathNames(
bytes,
connector.allContacts,
connector.pathHashByteWidth,
);
case _RoutingMode.auto:
if (contact.pathLength < 0) return l10n.routing_noPathYet;
if (contact.pathLength == 0) return l10n.routing_directNoHops;
if (contact.path.isEmpty) {
return l10n.chat_hopsCount(contact.pathLength);
}
return PathHelper.resolvePathNames(
contact.path,
connector.allContacts,
connector.pathHashByteWidth,
);
}
}
Uint8List _displayBytes(Contact contact, _RoutingMode mode) {
return switch (mode) {
_RoutingMode.flood => Uint8List(0),
_RoutingMode.manual => contact.pathOverrideBytes ?? Uint8List(0),
_RoutingMode.auto => contact.path,
};
}
void _openPathTrace(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
List<int> pathBytes,
) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes),
flipPathAround: true,
targetContact: contact,
pathHashByteWidth: connector.pathHashByteWidth,
),
),
);
}
void _showPathDetail(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
List<int> pathBytes,
) {
final l10n = context.l10n;
final formattedPath = PathHelper.splitPathBytes(
pathBytes,
connector.pathHashByteWidth,
).map(PathHelper.formatHopHex).join(',');
final resolvedNames = PathHelper.resolvePathNames(
pathBytes,
connector.allContacts,
connector.pathHashByteWidth,
);
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l10n.chat_fullPath),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(formattedPath),
const SizedBox(height: 8),
SelectableText(
resolvedNames,
style: TextStyle(
fontSize: 13,
color: Theme.of(dialogContext).colorScheme.onSurfaceVariant,
),
),
],
),
actions: [
TextButton(
onPressed: () =>
_openPathTrace(dialogContext, connector, contact, pathBytes),
child: Text(l10n.contacts_pathTrace),
),
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.common_close),
),
],
),
);
}
Widget _currentRouteCard(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
({
int successCount,
int failureCount,
int lastTripTimeMs,
DateTime? lastUsed,
})?
floodStats,
) {
final l10n = context.l10n;
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final displayBytes = _displayBytes(contact, mode);
return Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
mode == _RoutingMode.flood ? Icons.waves : Icons.route,
size: 18,
color: scheme.primary,
),
const SizedBox(width: 8),
Text(
l10n.routing_currentRoute,
style: theme.textTheme.titleSmall,
),
],
),
const SizedBox(height: 8),
Text(
_routeText(context, connector, contact, mode),
style: theme.textTheme.bodyMedium,
),
if (mode == _RoutingMode.flood &&
floodStats != null &&
(floodStats.successCount > 0 || floodStats.failureCount > 0))
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_floodStatsLine(context, floodStats),
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
),
if (_syncStatus != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_syncStatus!,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.tertiary,
),
),
),
Wrap(
spacing: 8,
children: [
if (displayBytes.isNotEmpty)
TextButton.icon(
icon: const Icon(Icons.map_outlined, size: 18),
label: Text(l10n.contacts_pathTrace),
onPressed: () => _openPathTrace(
context,
connector,
contact,
displayBytes,
),
),
if (mode == _RoutingMode.manual)
TextButton.icon(
icon: const Icon(Icons.edit, size: 18),
label: Text(l10n.routing_editPath),
onPressed: () => _editManualPath(connector, contact),
),
if (mode == _RoutingMode.auto && contact.pathLength >= 0)
TextButton.icon(
icon: const Icon(Icons.restart_alt, size: 18),
label: Text(l10n.routing_forgetPath),
onPressed: () => _forgetPath(connector, contact),
),
],
),
],
),
),
);
}
String _floodStatsLine(
BuildContext context,
({
int successCount,
int failureCount,
int lastTripTimeMs,
DateTime? lastUsed,
})
stats,
) {
final l10n = context.l10n;
final parts = <String>[
l10n.routing_deliveryCounts(stats.successCount, stats.failureCount),
if (stats.lastTripTimeMs > 0)
'${(stats.lastTripTimeMs / 1000).toStringAsFixed(1)}s',
if (stats.lastUsed != null)
l10n.routing_lastWorked(_relativeTime(context, stats.lastUsed!)),
];
return parts.join('');
}
Widget _floodTile(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
({
int successCount,
int failureCount,
int lastTripTimeMs,
DateTime? lastUsed,
})
stats,
) {
final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme;
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
radius: 18,
backgroundColor: scheme.surfaceContainerHighest,
child: Icon(Icons.waves, size: 18, color: scheme.onSurfaceVariant),
),
title: Text(l10n.routing_floodDelivery),
subtitle: Text(
_floodStatsLine(context, stats),
style: const TextStyle(fontSize: 11),
),
trailing: mode == _RoutingMode.flood
? Icon(
Icons.check_circle,
color: scheme.primary,
semanticLabel: l10n.routing_inUse,
)
: null,
onTap: mode == _RoutingMode.flood
? null
: () => _selectMode(connector, contact, _RoutingMode.flood),
),
);
}
Widget _pathRecordTile(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
PathHistoryService pathService,
PathRecord record,
_PathQuality quality,
) {
final l10n = context.l10n;
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final (Color bg, Color fg) = switch (quality) {
_PathQuality.strong => (
scheme.primaryContainer,
scheme.onPrimaryContainer,
),
_PathQuality.good => (
scheme.secondaryContainer,
scheme.onSecondaryContainer,
),
_PathQuality.fair => (
scheme.tertiaryContainer,
scheme.onTertiaryContainer,
),
_PathQuality.proven => (
scheme.primaryContainer,
scheme.onPrimaryContainer,
),
_ => (scheme.surfaceContainerHighest, scheme.onSurfaceVariant),
};
final hasBytes = record.pathBytes.isNotEmpty;
final inUse =
hasBytes &&
((mode == _RoutingMode.manual &&
listEquals(record.pathBytes, contact.pathOverrideBytes)) ||
(mode == _RoutingMode.auto &&
listEquals(record.pathBytes, contact.path)));
final title = hasBytes
? PathHelper.resolvePathNames(
record.pathBytes,
connector.allContacts,
connector.pathHashByteWidth,
)
: l10n.chat_hopsCount(record.hopCount);
final line1 =
'${l10n.chat_hopsCount(record.hopCount)}${_qualityLabel(context, quality)}';
final line2Parts = <String>[
record.timestamp != null
? l10n.routing_lastWorked(_relativeTime(context, record.timestamp!))
: l10n.routing_neverWorked,
if (record.tripTimeMs > 0)
'${(record.tripTimeMs / 1000).toStringAsFixed(1)}s',
l10n.routing_deliveryCounts(record.successCount, record.failureCount),
];
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
enabled: hasBytes,
leading: CircleAvatar(
radius: 18,
backgroundColor: bg,
child: Icon(
_qualityIcon(quality),
size: 18,
color: fg,
semanticLabel: _qualityLabel(context, quality),
),
),
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(
'$line1\n${line2Parts.join('')}',
style: const TextStyle(fontSize: 11),
),
isThreeLine: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (inUse)
Tooltip(
message: l10n.routing_inUse,
child: Icon(
Icons.check_circle,
color: scheme.primary,
semanticLabel: l10n.routing_inUse,
),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
tooltip: l10n.chat_removePath,
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
onPressed: () => pathService.removePathRecord(
contact.publicKeyHex,
record.pathBytes,
),
),
],
),
onTap: hasBytes && !inUse
? () => _applyHistoryPath(connector, contact, record)
: null,
onLongPress: hasBytes
? () =>
_showPathDetail(context, connector, contact, record.pathBytes)
: null,
),
);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final scheme = theme.colorScheme;
return Consumer2<MeshCoreConnector, PathHistoryService>(
builder: (context, connector, pathService, _) {
final contact = _resolveContact(connector);
final mode = _modeOf(contact);
final floodStats = pathService.getFloodStats(contact.publicKeyHex);
final hasFloodStats =
floodStats != null &&
(floodStats.successCount > 0 || floodStats.failureCount > 0);
final rankedRepeaters = List.of(connector.directRepeaters)
..sort((a, b) => b.ranking.compareTo(a.ranking));
final entries =
pathService
.getRecentPaths(contact.publicKeyHex)
.map(
(r) => (
quality: _qualityOf(connector, r, rankedRepeaters),
record: r,
),
)
.toList()
..sort((a, b) {
final byQuality = a.quality.index.compareTo(b.quality.index);
if (byQuality != 0) return byQuality;
final aTime =
a.record.timestamp ??
DateTime.fromMillisecondsSinceEpoch(0);
final bTime =
b.record.timestamp ??
DateTime.fromMillisecondsSinceEpoch(0);
return bTime.compareTo(aTime);
});
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
children: [
Center(
child: Container(
width: 32,
height: 4,
decoration: BoxDecoration(
color: scheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 12),
Text(l10n.routing_title, style: theme.textTheme.titleLarge),
Text(
contact.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
SegmentedButton<_RoutingMode>(
style: const ButtonStyle(
minimumSize: WidgetStatePropertyAll(Size.fromHeight(44)),
),
segments: [
ButtonSegment(
value: _RoutingMode.auto,
icon: const Icon(Icons.auto_mode),
label: Text(l10n.routing_modeAuto),
),
ButtonSegment(
value: _RoutingMode.flood,
icon: const Icon(Icons.waves),
label: Text(l10n.routing_modeFlood),
),
ButtonSegment(
value: _RoutingMode.manual,
icon: const Icon(Icons.edit_road),
label: Text(l10n.routing_modeManual),
),
],
selected: {mode},
onSelectionChanged: (selection) =>
_selectMode(connector, contact, selection.first),
),
const SizedBox(height: 8),
Text(
_modeHint(context, mode),
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
_currentRouteCard(context, connector, contact, mode, floodStats),
const SizedBox(height: 16),
Text(l10n.routing_knownPaths, style: theme.textTheme.titleSmall),
Text(
l10n.routing_knownPathsHint,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
if (hasFloodStats)
_floodTile(context, connector, contact, mode, floodStats),
if (entries.isEmpty && !hasFloodStats)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
l10n.chat_noPathHistoryYet,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
),
...entries.map(
(entry) => _pathRecordTile(
context,
connector,
contact,
mode,
pathService,
entry.record,
entry.quality,
),
),
],
);
},
);
}
}
+1 -1
View File
@@ -180,7 +180,7 @@ class _SNRIndicatorState extends State<SNRIndicator> {
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
+428
View File
@@ -0,0 +1,428 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart';
class TelemetryLocationMap extends StatefulWidget {
final double latitude;
final double longitude;
final String label;
final int contactType;
final String contactPublicKeyHex;
const TelemetryLocationMap({
super.key,
required this.latitude,
required this.longitude,
required this.label,
required this.contactType,
required this.contactPublicKeyHex,
});
@override
State<TelemetryLocationMap> createState() => _TelemetryLocationMapState();
}
class _TelemetryLocationMapState extends State<TelemetryLocationMap> {
static const double _initialZoom = 14.0;
static const double _minZoom = 2.0;
static const double _maxZoom = 18.0;
final MapController _mapController = MapController();
LatLng get _position => LatLng(widget.latitude, widget.longitude);
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant TelemetryLocationMap oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.latitude == widget.latitude &&
oldWidget.longitude == widget.longitude) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(_position, _initialZoom);
}
});
}
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final settingsService = context.watch<AppSettingsService>();
final settings = settingsService.settings;
final tileCache = context.read<MapTileCacheService>();
final contacts = _filteredContacts(connector, settings);
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 4),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LayoutBuilder(
builder: (context, constraints) {
final maxHeight = MediaQuery.sizeOf(context).height * 0.75;
final squareHeight = constraints.maxWidth.isFinite
? constraints.maxWidth
: maxHeight;
// Prefer square sizing by width, but cap only the height so the map
// remains usable on wide screens without growing past the viewport.
final mapHeight = squareHeight > maxHeight
? maxHeight
: squareHeight;
return SizedBox(
width: double.infinity,
height: mapHeight,
child: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _position,
initialZoom: _initialZoom,
minZoom: _minZoom,
maxZoom: _maxZoom,
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(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
MarkerLayer(
markers: [
...contacts.map(_buildContactMarker),
_buildTelemetryMarker(),
_buildLabelMarker(_position, widget.label),
],
),
],
),
Positioned(
top: 8,
right: 8,
child: _MapButton(
icon: Icons.filter_list,
tooltip: context.l10n.map_filterNodes,
onPressed: () =>
_showFilterDialog(context, settingsService),
),
),
Positioned(
left: 8,
top: 8,
child: Column(
children: [
_MapButton(
icon: Icons.add,
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomBy(1),
),
const SizedBox(height: 6),
_MapButton(
icon: Icons.remove,
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomBy(-1),
),
const SizedBox(height: 6),
_MapButton(
icon: Icons.my_location,
tooltip: context.l10n.map_centerMap,
onPressed: () =>
_mapController.move(_position, _initialZoom),
),
],
),
),
],
),
);
},
),
),
);
}
List<Contact> _filteredContacts(
MeshCoreConnector connector,
AppSettings settings,
) {
final contacts = settings.mapShowDiscoveryContacts
? connector.allContacts
: connector.allContacts.where((contact) => contact.isActive).toList();
return contacts.where((contact) {
if (!contact.hasLocation) return false;
if (contact.publicKeyHex == widget.contactPublicKeyHex) return false;
if (contact.type == advTypeChat) return settings.mapShowChatNodes;
if (contact.type == advTypeRepeater) return settings.mapShowRepeaters;
return settings.mapShowOtherNodes;
}).toList();
}
Marker _buildTelemetryMarker() {
return Marker(
point: _position,
width: 44,
height: 44,
child: IgnorePointer(
child: _MarkerBubble(
color: Colors.red,
icon: _getNodeIcon(widget.contactType),
size: 24,
),
),
);
}
Marker _buildContactMarker(Contact contact) {
return Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 34,
height: 34,
child: IgnorePointer(
child: _MarkerBubble(
color: _getNodeColor(contact.type),
icon: _getNodeIcon(contact.type),
size: 18,
),
),
);
}
Marker _buildLabelMarker(LatLng point, String label) {
return Marker(
point: point,
width: 140,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -24),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
void _zoomBy(double delta) {
final camera = _mapController.camera;
final nextZoom = (camera.zoom + delta).clamp(_minZoom, _maxZoom).toDouble();
_mapController.move(camera.center, nextZoom);
}
bool _isDesktopPlatform(TargetPlatform platform) {
return platform == TargetPlatform.linux ||
platform == TargetPlatform.windows ||
platform == TargetPlatform.macOS;
}
Color _getNodeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.green;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.orange;
default:
return Colors.grey;
}
}
IconData _getNodeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.person;
case advTypeRepeater:
return Icons.router;
case advTypeRoom:
return Icons.meeting_room;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
void _showFilterDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.map_filterNodes),
content: SingleChildScrollView(
child: Consumer<AppSettingsService>(
builder: (consumerContext, service, child) {
final settings = service.settings;
// Reuse the global map filters so the telemetry preview and the
// main map stay consistent without another settings model.
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CheckboxListTile(
title: Text(context.l10n.map_chatNodes),
value: settings.mapShowChatNodes,
onChanged: (value) {
service.setMapShowChatNodes(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_repeaters),
value: settings.mapShowRepeaters,
onChanged: (value) {
service.setMapShowRepeaters(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_otherNodes),
value: settings.mapShowOtherNodes,
onChanged: (value) {
service.setMapShowOtherNodes(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showDiscoveryContacts),
value: settings.mapShowDiscoveryContacts,
onChanged: (value) {
service.setMapShowDiscoveryContacts(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
],
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_close),
),
],
),
);
}
}
class _MarkerBubble extends StatelessWidget {
final Color color;
final IconData icon;
final double size;
const _MarkerBubble({
required this.color,
required this.icon,
required this.size,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Icon(icon, color: Colors.white, size: size),
);
}
}
class _MapButton extends StatelessWidget {
final IconData icon;
final String tooltip;
final VoidCallback onPressed;
const _MapButton({
required this.icon,
required this.tooltip,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
elevation: 3,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: IconButton(
icon: Icon(icon),
tooltip: tooltip,
onPressed: onPressed,
constraints: const BoxConstraints.tightFor(width: 40, height: 40),
padding: EdgeInsets.zero,
iconSize: 20,
),
);
}
}
-52
View File
@@ -1,73 +1,21 @@
PODS:
- flserial (0.0.1):
- FlutterMacOS
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- flserial (from `Flutter/ephemeral/.symlinks/plugins/flserial/macos`)
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- 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`)
EXTERNAL SOURCES:
flserial:
:path: Flutter/ephemeral/.symlinks/plugins/flserial/macos
flutter_blue_plus_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin
flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
FlutterMacOS:
:path: Flutter/ephemeral
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
flserial: 3c161e076dfc73458ec5803e7a9a9d2bb85fadf6
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
+22
View File
@@ -29,6 +29,7 @@
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
99C5B380294D2DE19A818101 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */; };
D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -88,6 +89,7 @@
BEFF4DDC60AFB628205F8E82 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -103,6 +105,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -164,6 +167,7 @@
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
@@ -231,6 +235,9 @@
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
@@ -256,6 +263,9 @@
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
@@ -796,6 +806,18 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}
@@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "meshcore_open.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
+6 -6
View File
@@ -48,7 +48,7 @@ dependencies:
uuid: ^4.3.3
flutter_map: ^8.2.2
latlong2: ^0.9.1
flutter_local_notifications: ^20.1.0
flutter_local_notifications: ^22.0.0
crypto: ^3.0.3
pointycastle: ^4.0.0
http: ^1.2.0
@@ -56,22 +56,22 @@ dependencies:
flutter_cache_manager: ^3.4.1
flutter_foreground_task: ^9.2.0
characters: ^1.4.0
package_info_plus: ^9.0.0
package_info_plus: ^10.1.0
mobile_scanner: ^7.1.4 # QR/barcode scanning
qr_flutter: ^4.1.0 # QR code generation
url_launcher: ^6.3.0 # Launch URLs in system browser
flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text
gpx: ^2.3.0
path_provider: ^2.1.5
share_plus: ^12.0.1
share_plus: ^13.1.0
build_pipe: ^0.3.1
material_symbols_icons: ^4.2906.0
material_symbols_icons: ^4.2928.1
web: ^1.1.1
flutter_svg: ^2.0.10+1
flutter_blue_plus_platform_interface: ^8.2.1
flutter_blue_plus_platform_interface: ^9.0.2
ml_algo: ^16.0.0
ml_dataframe: ^1.0.0
llamadart: '>=0.6.8 <0.7.0'
llamadart: ^0.8.0
flutter_langdetect: ^0.0.1
hooks:
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Generate local PoCs for MeshCore clipboard contact import validation gaps.
The output is a meshcore:// URI suitable for manual testing in a local/dev app
session. It does not connect to BLE/USB/TCP devices or transmit anything.
"""
from __future__ import annotations
import argparse
import sys
SCHEME = "meshcore://"
def uri_from_bytes(payload: bytes) -> str:
return SCHEME + payload.hex()
def oversized_payload(size: int) -> bytes:
if size < 1:
raise ValueError("size must be positive")
return b"A" * size
def short_malformed_payload() -> bytes:
return b"\x00"
def non_advert_like_payload() -> bytes:
# 98 bytes matches the UI's minimum exported-contact length check, but the
# content is intentionally not a valid signed advert/contact packet.
payload = bytearray(98)
payload[0:4] = b"POC!"
payload[36] = 0xFF
payload[-4:] = b"END!"
return bytes(payload)
def main() -> int:
parser = argparse.ArgumentParser(
description="Generate meshcore:// clipboard import PoC payloads."
)
parser.add_argument(
"case",
choices=("short", "non-advert", "oversized"),
help="PoC case to generate.",
)
parser.add_argument(
"--size",
type=int,
default=4096,
help="Byte length for the oversized case. Default: 4096.",
)
args = parser.parse_args()
if args.case == "short":
payload = short_malformed_payload()
elif args.case == "non-advert":
payload = non_advert_like_payload()
else:
payload = oversized_payload(args.size)
sys.stdout.write(uri_from_bytes(payload))
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+4 -2
View File
@@ -123,11 +123,13 @@ void main() {
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FloatingActionButton, 'TCP'));
final scannerContext = tester.element(find.byType(ScannerScreen));
final scannerL10n = AppLocalizations.of(scannerContext);
await tester.tap(find.byTooltip(scannerL10n.connectionChoiceTcpLabel));
await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsOneWidget);
await tester.tap(find.widgetWithText(FloatingActionButton, 'Bluetooth'));
await tester.tap(find.widgetWithText(OutlinedButton, 'Bluetooth'));
await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsNothing);
+7 -5
View File
@@ -157,10 +157,12 @@ void main() {
);
await tester.pumpAndSettle();
final context = tester.element(find.byType(ScannerScreen));
final l10n = AppLocalizations.of(context);
if (PlatformInfo.supportsUsbSerial) {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget);
expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsOneWidget);
} else {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing);
expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsNothing);
}
// ScannerScreen.dispose() schedules disconnect work that debounces notify.
@@ -186,13 +188,13 @@ void main() {
final context = tester.element(find.byType(ScannerScreen));
final l10n = AppLocalizations.of(context);
expect(find.text(l10n.scanner_scan), findsOneWidget);
expect(find.text(l10n.scanner_scan), findsWidgets);
if (PlatformInfo.supportsUsbSerial) {
expect(find.text(l10n.connectionChoiceUsbLabel), findsOneWidget);
expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsOneWidget);
}
if (!PlatformInfo.isWeb) {
expect(find.text(l10n.connectionChoiceTcpLabel), findsOneWidget);
expect(find.byTooltip(l10n.connectionChoiceTcpLabel), findsOneWidget);
}
await tester.pumpWidget(const SizedBox.shrink());