diff --git a/.gitignore b/.gitignore index fa4d28d6..6e4fb526 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ keystore.properties # Cloudflare Wrangler .wrangler + +# Claude Code local working dir (worktrees, jobs, settings) +.claude/ diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 82af67b1..bb9c5370 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -195,6 +195,7 @@ class MeshCoreConnector extends ChangeNotifier { {}; // contactPubKeyHex -> Set of "targetHash_emoji" StreamSubscription>? _scanSubscription; + StreamSubscription? _isScanningSubscription; StreamSubscription? _connectionSubscription; StreamSubscription>? _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 resendMessage(Contact contact, Message message) async { + await deleteMessage(message); + await sendMessage( + contact, + message.text, + originalText: message.originalText, + translatedLanguageCode: message.translatedLanguageCode, + translationModelId: message.translationModelId, + ); + } + Future _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 _loadLinuxSystemDevicesForScan() async { @@ -1465,31 +1513,28 @@ 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( - (device) => ScanResult( - device: device, - advertisementData: AdvertisementData( - advName: device.platformName, - txPowerLevel: null, - appearance: null, - connectable: true, - manufacturerData: const >{}, - serviceData: const >{}, - serviceUuids: [Guid(MeshCoreUuids.service)], - ), - rssi: 0, - timeStamp: DateTime.now(), - ), + systemDevices.map( + (device) => ScanResult( + device: device, + advertisementData: AdvertisementData( + advName: device.platformName, + txPowerLevel: null, + appearance: null, + connectable: true, + manufacturerData: const >{}, + serviceData: const >{}, + serviceUuids: [Guid(MeshCoreUuids.service)], ), + rssi: 0, + timeStamp: DateTime.now(), + ), + ), ); _mergeLinuxSystemScanResults(); notifyListeners(); @@ -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 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,18 +2010,71 @@ class MeshCoreConnector extends ChangeNotifier { } } } else { - try { - await device.connect( + Future 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', ); - rethrow; + 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.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; + } } } @@ -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 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 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(); diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index c271e31e..fc46d362 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -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(); } diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart index 084cc424..0a32b2a6 100644 --- a/lib/connector/meshcore_uuids.dart +++ b/lib/connector/meshcore_uuids.dart @@ -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 deviceNamePrefixes = [ "MeshCore-", "Whisper-", diff --git a/lib/helpers/cayenne_lpp.dart b/lib/helpers/cayenne_lpp.dart index 07909e63..021e9cc5 100644 --- a/lib/helpers/cayenne_lpp.dart +++ b/lib/helpers/cayenne_lpp.dart @@ -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> 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 - >[]; // 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> _sortedChannelValues( + Map> 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(); + } } diff --git a/lib/helpers/contact_ui.dart b/lib/helpers/contact_ui.dart new file mode 100644 index 00000000..9e3b31f7 --- /dev/null +++ b/lib/helpers/contact_ui.dart @@ -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(); +} diff --git a/lib/helpers/path_helper.dart b/lib/helpers/path_helper.dart index 935fa2bc..198f0530 100644 --- a/lib/helpers/path_helper.dart +++ b/lib/helpers/path_helper.dart @@ -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 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 hopBytes) { return hopBytes .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) .join(); } + static String? hopName(int byte, List 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 splitPathBytes( List 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(); diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index baf0bc3f..50815be5 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -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": "Извлечена позиция" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 5b717e02..d8160ba2 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index af1bdb24..4cbd7a29 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 8d76f576..179abae6 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b46d0b57..25960e1c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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" } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index c4576038..589e0793 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -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" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a8e1d4ee..674c558d 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index def330d2..4afea945 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -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": "デフォルトの地域範囲を設定します。「」を使用すると、設定をリセットできます。", - "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": "推測される位置" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index a9f4d494..56d76aaf 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -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": "추론된 위치" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e25ede88..96f41c08 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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 diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 4e564088..910218cb 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -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 => 'Извлечена позиция'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1bea3ca0..353d2ec2 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a2ecac1a..6c049586 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 356d9e49..a2950ad6 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 158ebac7..8b6aa669 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 802da6c9..3f0d3c53 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 078fc96d..76afeb6b 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 9ebae739..1a0ab75f 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -92,6 +92,24 @@ class AppLocalizationsJa 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 AppLocalizationsJa extends AppLocalizations { return '$percent%'; } + @override + String get common_autoRefresh => '自動更新'; + + @override + String get common_interval => '間隔'; + @override String get scanner_title => 'MeshCore オープン'; @@ -553,7 +577,7 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_txPowerInvalid => '無効な送信電力 (0-22 dBm)'; @override - String get settings_clientRepeat => 'オフグリッド(電力網から孤立した状態)の繰り返し'; + String get settings_clientRepeat => 'オフグリッドリピータ'; @override String get settings_clientRepeatSubtitle => @@ -715,7 +739,7 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_autoRouteRotation => '自動ルートの切り替え'; @override - String get appSettings_autoRouteRotationSubtitle => '最適なルートと、洪水モードを切り替える'; + String get appSettings_autoRouteRotationSubtitle => '最適なルートと、フラッドモードを切り替える'; @override String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています'; @@ -757,11 +781,6 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_maxMessageRetriesSubtitle => 'メッセージを「失敗」とマークするまでの、再試行回数'; - @override - String path_routeWeight(String weight, String max) { - return '$weight/$max'; - } - @override String get appSettings_battery => 'バッテリー'; @@ -792,7 +811,7 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_showRepeaters => '繰り返し再生機能'; @override - String get appSettings_showRepeatersSubtitle => '地図上にリピーターノードを表示する'; + String get appSettings_showRepeatersSubtitle => '地図上にリピータノードを表示する'; @override String get appSettings_showChatNodes => 'チャットノードの表示'; @@ -931,7 +950,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get contacts_manageRepeater => 'リピーターの管理'; + String get contacts_manageRepeater => 'リピータの管理'; @override String get contacts_manageRoom => 'ルームサーバーの管理'; @@ -956,6 +975,15 @@ class AppLocalizationsJa 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 => 'グループ名'; @@ -1422,33 +1450,6 @@ class AppLocalizationsJa 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( @@ -1460,61 +1461,146 @@ class AppLocalizationsJa 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つのハッシュ文字を、カンマで区切って記述'; + + @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 => 'ローカルで保存。同期のために接続する。'; @@ -1537,7 +1623,7 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する'; @override - String get chat_floodForced => '洪水(強制的な)'; + String get chat_floodForced => 'フラッド(強制的な)'; @override String get chat_directForced => '直接的な(強制的な)'; @@ -1548,7 +1634,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get chat_floodAuto => '洪水 (自動)'; + String get chat_floodAuto => 'フラッド (自動)'; @override String get chat_direct => '直接'; @@ -1613,7 +1699,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_chat => 'チャット'; @override - String get map_repeater => '繰り返し送信装置'; + String get map_repeater => 'リピータ'; @override String get map_room => '部屋'; @@ -1708,13 +1794,13 @@ class AppLocalizationsJa extends AppLocalizations { String get map_chatNodes => 'チャットノード'; @override - String get map_repeaters => '繰り返し送信装置'; + String get map_repeaters => 'リピータ'; @override String get map_otherNodes => 'その他のノード'; @override - String get map_showOverlaps => 'リピーターキーの重複'; + String get map_showOverlaps => 'リピータキーの重複'; @override String get map_keyPrefix => '主要なプレフィックス'; @@ -1753,7 +1839,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_joinRoom => '部屋に参加する'; @override - String get map_manageRepeater => 'リピーターの管理'; + String get map_manageRepeater => 'リピータの管理'; @override String get map_tapToAdd => 'ノードをクリックして、パスに追加します。'; @@ -1926,7 +2012,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get login_repeaterDescription => - '設定やステータスにアクセスするために、リピーターのパスワードを入力してください。'; + '設定やステータスにアクセスするために、リピータのパスワードを入力してください。'; @override String get login_roomDescription => '設定やステータスへのアクセスには、部屋のパスワードを入力してください。'; @@ -1941,7 +2027,7 @@ class AppLocalizationsJa extends AppLocalizations { String get login_autoUseSavedPath => '自動 (保存されたパスを使用)'; @override - String get login_forceFloodMode => '強制的に洪水モードを起動'; + String get login_forceFloodMode => '強制的にフラッドモードを起動'; @override String get login_managePaths => 'パスの管理'; @@ -1969,70 +2055,20 @@ class AppLocalizationsJa 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: 'ホップ', - one: 'ホップ', - ); - return '$count $_temp0のパスを使用'; - } - - @override - String get path_enterCustomPath => 'カスタムパスを入力'; - @override String get path_currentPathLabel => '現在の経路'; - @override - String get path_hexPrefixInstructions => - '各ホップに対して、2文字の16進数プレフィックスをカンマで区切って入力してください。'; - - @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 => 'リピーター管理'; + String get repeater_management => 'リピータ管理'; @override String get room_management => 'ルームサーバーの管理'; @override - String get repeater_guest => '繰り返し送信に関する情報'; + String get repeater_guest => 'リピータに関する情報'; @override String get room_guest => 'ルームサーバーに関する情報'; @@ -2047,7 +2083,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_status => 'ステータス'; @override - String get repeater_statusSubtitle => 'リピーターの状態、統計情報、および隣接するネットワークの情報を表示する'; + String get repeater_statusSubtitle => 'リピータの状態、統計情報、および隣接するネットワークの情報を表示する'; @override String get repeater_telemetry => 'テレメトリー'; @@ -2059,7 +2095,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cli => 'CLI(コマンドラインインターフェース)'; @override - String get repeater_cliSubtitle => 'リピーターへのコマンドを送信する'; + String get repeater_cliSubtitle => 'リピータへのコマンドを送信する'; @override String get repeater_neighbors => '近隣住民'; @@ -2071,7 +2107,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_settings => '設定'; @override - String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する'; + String get repeater_settingsSubtitle => 'リピータのパラメータを設定する'; @override String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する'; @@ -2086,15 +2122,6 @@ class AppLocalizationsJa 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 => 'リフレッシュ'; @@ -2169,12 +2196,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return '合計: $total, 洪水: $flood, 直接: $direct'; + return '合計: $total, フラッド: $flood, 直接: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return '合計: $total, 洪水: $flood, 直接: $direct'; + return '合計: $total, フラッド: $flood, 直接: $direct'; } @override @@ -2188,16 +2215,16 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_settingsTitle => 'リピーター設定'; + String get repeater_settingsTitle => 'リピータ設定'; @override String get repeater_basicSettings => '基本設定'; @override - String get repeater_repeaterName => '送信装置名'; + String get repeater_repeaterName => 'リピータ名'; @override - String get repeater_repeaterNameHelper => 'このリピーターの名前'; + String get repeater_repeaterNameHelper => 'このリピータの名前'; @override String get repeater_adminPassword => '管理者パスワード'; @@ -2257,7 +2284,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_packetForwarding => 'パケット転送'; @override - String get repeater_packetForwardingSubtitle => 'リピーターがパケットを転送できるように設定する'; + String get repeater_packetForwardingSubtitle => 'リピータがパケットを転送できるように設定する'; @override String get repeater_guestAccess => 'ゲストへのアクセス'; @@ -2283,7 +2310,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => '洪水に関する広告の表示間隔'; + String get repeater_floodAdvertInterval => 'フラッドに関する広告の表示間隔'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2297,13 +2324,13 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_dangerZone => '危険区域'; @override - String get repeater_rebootRepeater => 'リピーターを再起動する'; + String get repeater_rebootRepeater => 'リピータを再起動する'; @override - String get repeater_rebootRepeaterSubtitle => 'リピーターデバイスを再起動する'; + String get repeater_rebootRepeaterSubtitle => 'リピータデバイスを再起動する'; @override - String get repeater_rebootRepeaterConfirm => '本当にこのリピーターを再起動したいですか?'; + String get repeater_rebootRepeaterConfirm => '本当にこのリピータを再起動したいですか?'; @override String get repeater_regenerateIdentityKey => 'IDキーの再生成'; @@ -2313,17 +2340,17 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'これにより、リピーターには新しい識別情報が割り当てられます。続行しますか?'; + 'これにより、リピータには新しい識別情報が割り当てられます。続行しますか?'; @override String get repeater_eraseFileSystem => 'ファイルシステムを削除する'; @override - String get repeater_eraseFileSystemSubtitle => 'リピーターファイルシステムをフォーマットする'; + String get repeater_eraseFileSystemSubtitle => 'リピータファイルシステムをフォーマットする'; @override String get repeater_eraseFileSystemConfirm => - '警告:この操作により、リピーター内のすべてのデータが消去されます。この操作は元に戻すことができません!'; + '警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!'; @override String get repeater_eraseSerialOnly => 'Erase機能は、シリアルコンソール経由でのみ利用可能です。'; @@ -2369,7 +2396,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_loopDetect => 'ループ検出'; @override - String get repeater_loopDetectHelper => 'ルーティングループに見えるような、洪水パケットを送信する'; + String get repeater_loopDetectHelper => 'ルーティングループを検知する'; @override String get repeater_loopDetectOff => 'オフ'; @@ -2398,7 +2425,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_ownerInfo => '事業者の情報'; @override - String get repeater_ownerInfoHelper => 'このリピーターに関する公開メタデータ'; + String get repeater_ownerInfoHelper => 'このリピータに関する公開メタデータ'; @override String get repeater_refreshOwnerInfo => 'オペレーター情報の更新'; @@ -2407,7 +2434,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_floodMax => '最大ホップ数'; @override - String get repeater_floodMaxHelper => '洪水パケットが移動できる最大ホップ数 (0-64)'; + String get repeater_floodMaxHelper => 'フラッドパケットが移動できる最大ホップ数 (0-64)'; @override String get repeater_advancedSettings => '高度な'; @@ -2420,14 +2447,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String get 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バイト経路に設定できます。'; + 'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。'; @override String get repeater_txDelay => 'フロイド・TXでの遅延'; @override String get repeater_txDelayHelper => - '洪水時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。'; + 'フラッド時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。'; @override String get repeater_directTxDelay => '直接的なTX遅延'; @@ -2454,16 +2481,16 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_actionsTitle => '行動'; @override - String get repeater_sendAdvert => '洪水に関する広告を送信'; + String get repeater_sendAdvert => 'フラッドに関する広告を送信'; @override - String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、洪水に関する広告を放送する'; + String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、フラッドに関する広告を放送する'; @override String get repeater_sendAdvertZeroHop => 'ゼロホップ形式の広告を送信する'; @override - String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(中継なし)'; + String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(リピータなし)'; @override String get repeater_clockSync => '現在、時刻を同期する'; @@ -2483,7 +2510,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_settingsSavedRebootNeeded => - '設定を保存しました — リピーターを再起動して適用してください'; + '設定を保存しました — リピータを再起動して適用してください'; @override String repeater_settingsPartialFailure(String failures) { @@ -2524,7 +2551,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_cliTitle => 'リピーターのコマンドラインインターフェース'; + String get repeater_cliTitle => 'リピータのコマンドラインインターフェース'; @override String get repeater_debugNextCommand => '次のコマンドのデバッグ'; @@ -2615,7 +2642,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpSetTx => 'LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)'; @override - String get repeater_cliHelpSetRepeat => 'このノードに対するリピーターの役割を有効化または無効化します。'; + String get repeater_cliHelpSetRepeat => 'このノードに対するリピータの役割を有効化または無効化します。'; @override String get repeater_cliHelpSetAllowReadOnly => @@ -2623,7 +2650,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpSetFloodMax => - 'インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。'; + 'インバウンドフラッドパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。'; @override String get repeater_cliHelpSetIntThresh => @@ -2642,7 +2669,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpSetFloodAdvertInterval => - '洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。'; + 'フラッド広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。'; @override String get repeater_cliHelpSetGuestPassword => @@ -2716,7 +2743,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'ゼロホップ広告を通じて受信した他のリピーターノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。'; + 'ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。'; @override String get repeater_cliHelpNeighborRemove => @@ -2724,7 +2751,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpRegion => - '(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。'; + '(特定のシリーズのみ)定義されたすべての地域と、現在のフラッド許可状況を一覧表示します。'; @override String get repeater_cliHelpRegionLoad => @@ -2743,7 +2770,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpRegionAllowf => - '指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)'; + '指定された領域に対して、「フラッド」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)'; @override String get repeater_cliHelpRegionDenyf => @@ -2799,7 +2826,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_logging => 'ログ記録'; @override - String get repeater_neighborsRepeaterOnly => '近隣住民(リピーターのみ)'; + String get repeater_neighborsRepeaterOnly => '近隣住民(リピータのみ)'; @override String get repeater_regionManagementRepeaterOnly => '地域管理(ブロードキャスト用のみ)'; @@ -2844,7 +2871,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpDiscoverNeighbors => - '近隣のノードに対して、ノードの探索リクエストを送信します。(中継機能のみ)'; + '近隣のノードに対して、ノードの探索リクエストを送信します。(リピータ機能のみ)'; @override String get repeater_cliHelpPowersaving => '省電力モードがオンになっているかどうかを表示します。'; @@ -2893,7 +2920,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetName => '設定されたノードの名前を表示します。'; @override - String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピーター、ルームサーバーなど)を表示します。'; + String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。'; @override String get repeater_cliHelpGetPublicKey => 'デバイスの公開鍵を表示します。'; @@ -2903,7 +2930,7 @@ class AppLocalizationsJa extends AppLocalizations { '(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。'; @override - String get repeater_cliHelpGetRepeat => 'パケット転送(リピーター機能)が有効になっているかどうかを表示します。'; + String get repeater_cliHelpGetRepeat => 'パケット転送(リピータ機能)が有効になっているかどうかを表示します。'; @override String get repeater_cliHelpGetTx => '現在のTX(送信)電力のdBm値を表示します。'; @@ -2942,7 +2969,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpGetFloodAdvertInterval => - '洪水に関する広告の放送時間を時間単位で表示します。'; + 'フラッドに関する広告の放送時間を時間単位で表示します。'; @override String get repeater_cliHelpGetGuestPassword => '設定されたゲストパスワードを表示します。'; @@ -2957,13 +2984,13 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetRxDelay => 'rxdelay の基本値を表示します。'; @override - String get repeater_cliHelpGetTxDelay => '洪水モードにおける送信遅延の要因を示します。'; + String get repeater_cliHelpGetTxDelay => 'フラッドモードにおける送信遅延の要因を示します。'; @override String get repeater_cliHelpGetDirectTxDelay => 'ダイレクトモードの遅延要素を示します。'; @override - String get repeater_cliHelpGetFloodMax => '最大浸水範囲の回数を表示します。'; + String get repeater_cliHelpGetFloodMax => 'フラッドパケットの最大ホップ数を表示します。'; @override String get repeater_cliHelpGetOwnerInfo => '所有者の連絡先情報を表示します。'; @@ -2975,7 +3002,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetLoopDetect => 'ループ検出の感度を示す。'; @override - String get repeater_cliHelpGetAcl => '(シリアルのみ)リピーター上のアクセス制御設定を一覧表示します。'; + String get repeater_cliHelpGetAcl => '(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。'; @override String get repeater_cliHelpGetBridgeEnabled => '橋が有効になっているかどうかを表示します。'; @@ -3038,10 +3065,10 @@ class AppLocalizationsJa extends AppLocalizations { 'デフォルトの地域範囲を設定します。「」を使用すると、設定をリセットできます。'; @override - String get repeater_cliHelpRegionListAllowed => '洪水時の通行が許可されている地域の一覧'; + String get repeater_cliHelpRegionListAllowed => 'フラッド時の通行が許可されている地域の一覧'; @override - String get repeater_cliHelpRegionListDenied => '洪水による交通を遮断している地域の一覧'; + String get repeater_cliHelpRegionListDenied => 'フラッドによる交通を遮断している地域の一覧'; @override String get repeater_cliHelpStatsPackets => '(シリアルのみ)パケットレベルの統計情報を表示します。'; @@ -3106,6 +3133,139 @@ class AppLocalizationsJa extends AppLocalizations { return '$celsius℃ / $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 => '近隣住民のデータを受信'; @@ -3118,7 +3278,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get neighbors_repeatersNeighbors => '繰り返し送信する、近隣'; + String get neighbors_repeatersNeighbors => '近隣のリピータ'; @override String get neighbors_noData => '近隣のデータは利用できません。'; @@ -3143,7 +3303,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_otherObservedPaths => '観察されたその他の経路'; @override - String get channelPath_repeaterHops => 'ホップの繰り返し'; + String get channelPath_repeaterHops => 'リピータホップ'; @override String get channelPath_noHopDetails => 'このパッケージに関する詳細な情報は提供されていません。'; @@ -3158,7 +3318,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_timeLabel => '時間'; @override - String get channelPath_repeatsLabel => '繰り返し'; + String get channelPath_repeatsLabel => 'リピータ'; @override String channelPath_pathLabel(int index) { @@ -3190,7 +3350,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_unknownPath => '不明'; @override - String get channelPath_floodPath => '洪水'; + String get channelPath_floodPath => 'フラッド'; @override String get channelPath_directPath => '直接'; @@ -3209,7 +3369,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_mapTitle => '経路図'; @override - String get channelPath_noRepeaterLocations => 'この経路には、中継装置の設置場所がありません。'; + String get channelPath_noRepeaterLocations => 'この経路にリピータの位置情報はありません。'; @override String channelPath_primaryPath(int index) { @@ -3429,7 +3589,7 @@ class AppLocalizationsJa extends AppLocalizations { String get listFilter_users => '利用者'; @override - String get listFilter_repeaters => '繰り返し送信装置'; + String get listFilter_repeaters => 'リピータ'; @override String get listFilter_roomServers => 'ルーム用サーバー'; @@ -3629,10 +3789,10 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_ping => 'パング'; @override - String get contacts_repeaterPathTrace => 'リピーターまでの経路を追跡する'; + String get contacts_repeaterPathTrace => 'リピータまでの経路を追跡する'; @override - String get contacts_repeaterPing => 'PING 繰り返し'; + String get contacts_repeaterPing => 'リピータにPING'; @override String get contacts_roomPathTrace => '部屋のサーバーへの経路を追跡する'; @@ -3664,7 +3824,7 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_zeroHopAdvert => 'ゼロホップ広告'; @override - String get contacts_floodAdvert => '洪水に関する広告'; + String get contacts_floodAdvert => 'フラッドに関する広告'; @override String get contacts_copyAdvertToClipboard => '広告をクリップボードにコピー'; @@ -3735,7 +3895,7 @@ class AppLocalizationsJa extends AppLocalizations { String get notification_receivedNewMessage => '新しいメッセージを受信'; @override - String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピーター/ルームサーバー'; + String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピータ/ルームサーバー'; @override String get settings_gpxExportRepeatersSubtitle => @@ -3768,7 +3928,7 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_gpxExportError => 'エクスポート時にエラーが発生しました。'; @override - String get settings_gpxExportRepeatersRoom => '中継装置およびルームサーバーの設置場所'; + String get settings_gpxExportRepeatersRoom => 'リピータ/ルームサーバーの位置情報'; @override String get settings_gpxExportChat => '関連施設'; @@ -3784,7 +3944,7 @@ class AppLocalizationsJa extends AppLocalizations { 'meshcore-open GPX形式の地図データのエクスポート'; @override - String get snrIndicator_nearByRepeaters => '近くの電波中継局'; + String get snrIndicator_nearByRepeaters => '近くのリピータ'; @override String get snrIndicator_lastSeen => '最後に確認された場所'; @@ -3806,11 +3966,11 @@ class AppLocalizationsJa extends AppLocalizations { '利用者が自動的に発見したユーザーを追加できるようにする。'; @override - String get contactsSettings_autoAddRepeatersTitle => '自動で繰り返し設定'; + String get contactsSettings_autoAddRepeatersTitle => 'リピータを自動追加'; @override String get contactsSettings_autoAddRepeatersSubtitle => - '発見した中継局を、自動的に追加できるようにする。'; + '発見したリピータを、自動的に追加できるようにする。'; @override String get contactsSettings_autoAddRoomServersTitle => '自動でルームサーバーを追加'; @@ -4085,4 +4245,28 @@ class AppLocalizationsJa 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 => '推測される位置'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 2a99bbf1..87073de1 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -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 => '추론된 위치'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2995afde..43b53fc2 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index b85fd254..7aee5717 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 21f03652..629f8faf 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 19937d4e..4a036002 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -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 => 'Выведенная позиция'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index b49b3904..3ee9cda9 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 6ca860fa..35ff41ef 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 533d42c1..c2961653 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -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'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 7f4f90bb..e78617c6 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -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 => 'Висновок щодо положення'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 88b252e5..7d42c766 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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 => '推测的位置'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 3a4a5acd..6e4482cb 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -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" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index e3b9704e..b431db95 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -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" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 2e8ef6cf..ff4283a1 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -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" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 2cd790d7..81275030 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -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": "Выведенная позиция" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 13ac1ce1..8a1220b6 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -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" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 1ee5a6eb..24c911bb 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -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" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index c2797395..ed0d55e0 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -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" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 5f4da699..8a3ddd38 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -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": "Висновок щодо положення" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 26f12c1d..f45db819 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -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": "推测的位置" } diff --git a/lib/main.dart b/lib/main.dart index 37aa29ff..1445bdde 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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, ), diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index ca6a6bf1..522a2c5c 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -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); } } } diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 9b0fb378..d1ccfd09 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -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( + 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,118 +349,154 @@ class AppSettingsScreen extends StatelessWidget { ); }, ), - if (settingsService.settings.autoRouteRotationEnabled) ...[ - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_maxRouteWeight), - subtitle: Column( + if (settingsService.settings.autoRouteRotationEnabled) + Container( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + padding: const EdgeInsets.only(left: 16), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(context.l10n.appSettings_maxRouteWeightSubtitle), - Slider( - value: settingsService.settings.maxRouteWeight, - min: 1, - max: 10, - divisions: 9, - label: settingsService.settings.maxRouteWeight - .round() - .toString(), - onChanged: (value) => - settingsService.setMaxRouteWeight(value), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxRouteWeight), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.appSettings_maxRouteWeightSubtitle), + Slider( + value: settingsService.settings.maxRouteWeight, + min: 1, + max: 10, + divisions: 9, + label: settingsService.settings.maxRouteWeight + .round() + .toString(), + onChanged: (value) => + settingsService.setMaxRouteWeight(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_initialRouteWeight), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_initialRouteWeightSubtitle, + ), + Slider( + value: settingsService.settings.initialRouteWeight, + min: 0.5, + max: 5.0, + divisions: 9, + label: settingsService.settings.initialRouteWeight + .toStringAsFixed(1), + onChanged: (value) => + settingsService.setInitialRouteWeight(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text( + context.l10n.appSettings_routeWeightSuccessIncrement, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightSuccessIncrementSubtitle, + ), + Slider( + value: settingsService + .settings + .routeWeightSuccessIncrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService + .settings + .routeWeightSuccessIncrement + .toStringAsFixed(1), + onChanged: (value) => settingsService + .setRouteWeightSuccessIncrement(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text( + context.l10n.appSettings_routeWeightFailureDecrement, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightFailureDecrementSubtitle, + ), + Slider( + value: settingsService + .settings + .routeWeightFailureDecrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService + .settings + .routeWeightFailureDecrement + .toStringAsFixed(1), + onChanged: (value) => settingsService + .setRouteWeightFailureDecrement(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxMessageRetries), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_maxMessageRetriesSubtitle, + ), + Slider( + value: settingsService.settings.maxMessageRetries + .toDouble(), + min: 2, + max: 10, + divisions: 8, + label: settingsService.settings.maxMessageRetries + .toString(), + onChanged: (value) => settingsService + .setMaxMessageRetries(value.toInt()), + ), + ], + ), ), ], ), ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_initialRouteWeight), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.appSettings_initialRouteWeightSubtitle), - Slider( - value: settingsService.settings.initialRouteWeight, - min: 0.5, - max: 5.0, - divisions: 9, - label: settingsService.settings.initialRouteWeight - .toStringAsFixed(1), - onChanged: (value) => - settingsService.setInitialRouteWeight(value), - ), - ], - ), + const Divider(height: 1), + SwitchListTile( + secondary: const Icon(Icons.location_searching), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_routeWeightSuccessIncrement), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context - .l10n - .appSettings_routeWeightSuccessIncrementSubtitle, - ), - Slider( - value: settingsService.settings.routeWeightSuccessIncrement, - min: 0.1, - max: 2.0, - divisions: 19, - label: settingsService.settings.routeWeightSuccessIncrement - .toStringAsFixed(1), - onChanged: (value) => - settingsService.setRouteWeightSuccessIncrement(value), - ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_routeWeightFailureDecrement), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context - .l10n - .appSettings_routeWeightFailureDecrementSubtitle, - ), - Slider( - value: settingsService.settings.routeWeightFailureDecrement, - min: 0.1, - max: 2.0, - divisions: 19, - label: settingsService.settings.routeWeightFailureDecrement - .toStringAsFixed(1), - onChanged: (value) => - settingsService.setRouteWeightFailureDecrement(value), - ), - ], - ), - ), - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_maxMessageRetries), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.appSettings_maxMessageRetriesSubtitle), - Slider( - value: settingsService.settings.maxMessageRetries - .toDouble(), - min: 2, - max: 10, - divisions: 8, - label: settingsService.settings.maxMessageRetries - .toString(), - onChanged: (value) => - settingsService.setMaxMessageRetries(value.toInt()), - ), - ], - ), - ), - ], + 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( - groupValue: settingsService.settings.themeMode, - onChanged: (value) { - if (value != null) { - settingsService.setThemeMode(value); - Navigator.pop(context); - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - title: Text(context.l10n.appSettings_themeSystem), - value: 'system', - ), - RadioListTile( - title: Text(context.l10n.appSettings_themeLight), - value: 'light', - ), - RadioListTile( - 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': diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 0f00769d..a2f4eb1d 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -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 { ) : widget.channel.name, style: const TextStyle(fontSize: 16), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), Consumer( builder: (context, connector, _) { @@ -312,9 +315,7 @@ class _ChannelChatScreenState extends State { icon: const Icon(Icons.more_vert), onSelected: (value) { if (value == 'clearChat') { - context.read().clearMessagesForChannel( - widget.channel.index, - ); + _confirmClearChat(); } }, itemBuilder: (context) => [ @@ -322,11 +323,17 @@ class _ChannelChatScreenState extends State { 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 { final messages = connector.getChannelMessages(widget.channel); if (messages.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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], - ), - ), - ], + return EmptyState( + icon: widget.channel.isPublicChannel + ? Icons.public + : Icons.tag, + 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 { 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 = {}; + final keyedIndices = {}; + 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 { } 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,181 +514,126 @@ class _ChannelChatScreenState extends State { const maxSwipeOffset = 64.0; const replySwipeThreshold = 64.0; const bodyFontSize = 14.0; - final messageBody = Column( - crossAxisAlignment: isOutgoing - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: isOutgoing - ? MainAxisAlignment.end - : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - _buildAvatar(message.senderName), - const SizedBox(width: 8), - ], - Flexible( - child: GestureDetector( - onTap: PlatformInfo.isDesktop - ? null - : () => _showMessagePathInfo(message), - onLongPress: () => _showMessageActions(message), - onSecondaryTapUp: PlatformInfo.isDesktop - ? (_) => _showMessageActions(message) - : null, - child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, - ), - decoration: BoxDecoration( - color: isOutgoing - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - top: 4, - bottom: 4, - ) - : EdgeInsets.zero, - child: Text( - message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, + final messageBody = LayoutBuilder( + builder: (context, constraints) => Column( + crossAxisAlignment: isOutgoing + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + _buildAvatar(message.senderName), + const SizedBox(width: 8), + ], + Flexible( + child: GestureDetector( + onLongPress: () => _showMessageActions(message), + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => _showMessageActions(message) + : null, + child: Container( + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.65, + ), + decoration: BoxDecoration( + color: isOutgoing + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + message.senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), ), ), - ), - if (gifId == null) const SizedBox(height: 4), - ], - if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message, textScale), - const SizedBox(height: 8), - ], - if (poi != null) - _buildPoiMessage( - context, - poi, - 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( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .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 - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible( - child: TranslatedMessageContent( - displayText: translatedDisplayText, - originalText: originalDisplayText, - style: TextStyle( - fontSize: bodyFontSize * textScale, - ), - originalStyle: TextStyle( - fontSize: bodyFontSize * textScale, - fontStyle: FontStyle.italic, - 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 (gifId == null) const SizedBox(height: 4), + ], + if (message.replyToMessageId != null) ...[ + _buildReplyPreview(message, textScale), + const SizedBox(height: 8), + ], + if (poi != null) + _buildPoiMessage( + context, + poi, + isOutgoing, + textScale, + message.senderName, + ) + else if (gifId != null) + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), ), ), ], - ], - ), - if (enableTracing) ...[ - if (displayPath.isNotEmpty) ...[ + ) + else + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: TranslatedMessageContent( + displayText: translatedDisplayText, + originalText: originalDisplayText, + style: TextStyle( + fontSize: bodyFontSize * textScale, + ), + originalStyle: TextStyle( + fontSize: bodyFontSize * textScale, + fontStyle: FontStyle.italic, + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.72), + ), + ), + ), + ], + ), + if (enableTracing && displayPath.isNotEmpty) ...[ const SizedBox(height: 4), Padding( padding: gifId != null @@ -687,8 +647,10 @@ class _ChannelChatScreenState extends State { ), ), style: TextStyle( - fontSize: 11, - color: Colors.grey[600], + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ), @@ -708,62 +670,69 @@ class _ChannelChatScreenState extends State { 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, ), ], ], ), ), ], - ], + ), ), ), ), + ], + ), + if (message.reactions.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), + child: _buildReactionsDisplay(message), ), ], - ), - if (message.reactions.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), - child: _buildReactionsDisplay(message), - ), ], - ], + ), ); if (!isOutgoing && !PlatformInfo.isDesktop) { @@ -965,9 +934,11 @@ class _ChannelChatScreenState extends State { 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().selfName ?? 'Me'; + final selfName = + context.read().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 { } 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 { ); } - 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 { 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,15 +1359,14 @@ class _ChannelChatScreenState extends State { _setReplyingTo(message); }, ), - if (PlatformInfo.isDesktop) - ListTile( - leading: const Icon(Icons.route), - title: Text(context.l10n.chat_path), - onTap: () { - Navigator.pop(sheetContext); - _showMessagePathInfo(message); - }, - ), + ListTile( + leading: const Icon(Icons.route), + title: Text(context.l10n.chat_path), + onTap: () { + Navigator.pop(sheetContext); + _showMessagePathInfo(message); + }, + ), // Can't react to your own messages if (!message.isOutgoing) ListTile( @@ -1470,19 +1409,21 @@ class _ChannelChatScreenState extends State { _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 { ); } + Future _confirmClearChat() async { + final confirmed = await showDialog( + 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().clearMessagesForChannel( + widget.channel.index, + ); + } + } + Future _deleteMessage(ChannelMessage message) async { await context.read().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( diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index bf3c201d..aab26df8 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -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,28 +747,32 @@ class _ChannelMessagePathMapScreenState markers.add( Marker( point: point, - width: 35, - height: 35, - child: Container( - decoration: BoxDecoration( - color: Colors.green, - 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), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + decoration: BoxDecoration( + color: Colors.green, + 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: Text( + hop.index.toString(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - hop.index.toString(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -768,28 +795,32 @@ class _ChannelMessagePathMapScreenState markers.add( Marker( point: selfPoint, - width: 35, - height: 35, - child: Container( - decoration: BoxDecoration( - color: Colors.teal, - 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), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + decoration: BoxDecoration( + color: Colors.teal, + 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: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -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,9 +902,25 @@ class _ChannelMessagePathMapScreenState children: [ Padding( padding: const EdgeInsets.all(12), - child: Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}', - style: const TextStyle(fontWeight: FontWeight.w600), + 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), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 60950798..cbb3cfaf 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -111,24 +111,26 @@ class _ChannelsScreenState extends State 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: [ - const Icon(Icons.groups), - const SizedBox(width: 8), - Text(context.l10n.community_manageCommunities), - ], - ), - onTap: () => _showManageCommunitiesDialog(context), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.groups), + const SizedBox(width: 8), + Text(context.l10n.community_manageCommunities), + ], ), + onTap: () => _showManageCommunitiesDialog(context), + ), PopupMenuItem( child: Row( children: [ @@ -241,32 +243,21 @@ class _ChannelsScreenState extends State ), Expanded( child: filteredChannels.isEmpty - ? ListView( - 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], - ), - const SizedBox(height: 16), - Text( - context.l10n.channels_noChannelsFound, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - ], + ? LayoutBuilder( + builder: (context, constraints) => ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: EmptyState( + icon: Icons.search_off, + title: context.l10n.channels_noChannelsFound, ), ), - ), - ], + ], + ), ) : (viewState.channelsSortOption == ChannelSortOption.manual && @@ -355,6 +346,9 @@ class _ChannelsScreenState extends State int? dragIndex, }) { final unreadCount = connector.getUnreadCountForChannel(channel); + final isMuted = context.watch().isChannelMuted( + channel.name, + ); // Determine icon and colors based on channel type IconData icon; @@ -447,37 +441,45 @@ class _ChannelsScreenState extends State 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: Icon( - Icons.drag_handle, - color: Theme.of(context).colorScheme.onSurfaceVariant, + 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( - builder: (context) => ChannelChatScreen( - channel: channel, - initialUnreadCount: unread, - ), + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelChatScreen( + channel: channel, + initialUnreadCount: unread, ), - ); - } + ), + ); }, onLongPress: () => _showChannelActions( context, @@ -538,10 +540,15 @@ class _ChannelsScreenState extends State }, ), 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 ? (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 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 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 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 final psk = community .deriveCommunityPublicPsk(); final channelName = - '${community.name} Public'; + '${community.name} $publicLabel'; connector.setChannel( nextIndex, channelName, @@ -1592,7 +1623,7 @@ class _ChannelsScreenState extends State }, 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 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 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 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 ), 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( @@ -1720,14 +1763,20 @@ class _ChannelsScreenState extends State 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 }, child: Text( dialogContext.l10n.community_delete, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 8618e0f6..89310bbd 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../utils/platform_info.dart'; @@ -15,35 +14,32 @@ import '../connector/meshcore_protocol.dart'; import '../helpers/cyr2lat.dart'; import '../helpers/reaction_helper.dart'; import '../widgets/message_status_icon.dart'; +import '../widgets/empty_state.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/gif_helper.dart'; -import '../helpers/path_helper.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; import '../l10n/contact_localization.dart'; import '../models/message.dart'; -import '../models/path_history.dart'; import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/path_history_service.dart'; import '../services/translation_service.dart'; import '../widgets/chat_zoom_wrapper.dart'; -import '../widgets/elements_ui.dart'; import '../widgets/byte_count_input.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; -import '../utils/emoji_utils.dart'; +import '../helpers/contact_ui.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; import '../widgets/message_translation_button.dart'; -import '../widgets/path_selection_dialog.dart'; +import '../widgets/routing_sheet.dart'; import '../widgets/radio_stats_entry.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/translated_message_content.dart'; -import '../utils/app_logger.dart'; import '../l10n/l10n.dart'; import '../helpers/snack_bar_builder.dart'; import '../widgets/unread_divider.dart'; @@ -187,28 +183,30 @@ class _ChatScreenState extends State { final unreadLabel = context.l10n.chat_unread(unreadCount); final pathLabel = _currentPathLabel(contact); - // Show path details if we have non-empty path data (from device or override) - final effectivePath = contact.pathOverrideBytes ?? contact.path; - final hasPathData = effectivePath.isNotEmpty; - return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(contact.name), + Text( + contact.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), GestureDetector( behavior: HitTestBehavior.opaque, - onTap: hasPathData - ? () => _showFullPathDialog(context, effectivePath) - : null, - child: Text( - '$pathLabel • $unreadLabel', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.normal, - decoration: hasPathData ? TextDecoration.underline : null, - decorationStyle: TextDecorationStyle.dotted, + onTap: () => + ContactRoutingSheet.show(context, contact: contact), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Text( + '$pathLabel • $unreadLabel', + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.normal, + decoration: TextDecoration.underline, + decorationStyle: TextDecorationStyle.dotted, + ), ), ), ), @@ -219,139 +217,44 @@ class _ChatScreenState extends State { centerTitle: false, bottom: const SyncProgressAppBarBottom(), actions: [ - Consumer( - builder: (context, connector, _) { - final contact = _resolveContact(connector); - final isFloodMode = contact.pathOverride == -1; - - final isDirectMode = contact.pathOverride == 0; - final activeMode = isFloodMode - ? 'flood' - : isDirectMode - ? 'direct' - : 'auto'; - - return PopupMenuButton( - icon: Icon(isFloodMode ? Icons.waves : Icons.route), - tooltip: context.l10n.chat_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(contact, pathLen: -1); - } else if (mode == 'direct') { - await connector.setPathOverride( - contact, - pathLen: 0, - pathBytes: Uint8List(0), - ); - } else { - await connector.setPathOverride(contact, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon( - Icons.auto_mode, - size: 20, - color: activeMode == 'auto' - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - context.l10n.chat_autoUseSavedPath, - style: TextStyle( - fontWeight: activeMode == 'auto' - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'direct', - child: Row( - children: [ - Icon( - Icons.near_me, - size: 20, - color: activeMode == 'direct' - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - context.l10n.chat_direct, - style: TextStyle( - fontWeight: activeMode == 'direct' - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon( - Icons.waves, - size: 20, - color: activeMode == 'flood' - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 8), - Text( - context.l10n.chat_forceFloodMode, - style: TextStyle( - fontWeight: activeMode == 'flood' - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ); - }, - ), - IconButton( - icon: const Icon(Icons.timeline), - tooltip: context.l10n.chat_pathManagement, - onPressed: () => _showPathHistory(context), - ), const RadioStatsIconButton(), Consumer( builder: (context, connector, _) { + final contact = _resolveContact(connector); + return PopupMenuButton( icon: const Icon(Icons.more_vert), onSelected: (value) { - if (value == 'info') { - _showContactInfo(context); - } - if (value == 'settings') { - _showContactSettings(context); - } - if (value == 'telemetry') { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - TelemetryScreen(contact: widget.contact), - ), - ); - } - if (value == 'clearChat') { - connector.clearMessagesForContact(widget.contact); + switch (value) { + case 'routing': + ContactRoutingSheet.show(context, contact: contact); + case 'info': + _showContactInfo(context); + case 'settings': + _showContactSettings(context); + case 'telemetry': + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TelemetryScreen(contact: widget.contact), + ), + ); + case 'clearChat': + _confirmClearChat(context, connector); } }, itemBuilder: (context) => [ + PopupMenuItem( + value: 'routing', + child: Row( + children: [ + const Icon(Icons.route, size: 20), + const SizedBox(width: 12), + Text(context.l10n.routing_title), + ], + ), + ), PopupMenuItem( value: 'info', child: Row( @@ -386,11 +289,17 @@ class _ChatScreenState extends State { 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, + ), ), ], ), @@ -425,24 +334,11 @@ class _ChatScreenState extends State { } Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.chat_bubble_outline, 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_sendMessageTo( - _resolveContact(context.read()).name, - ), - style: TextStyle(fontSize: 14, color: Colors.grey[500]), - ), - ], + return EmptyState( + icon: Icons.chat_bubble_outline, + title: context.l10n.chat_noMessages, + subtitle: context.l10n.chat_sendMessageTo( + _resolveContact(context.read()).name, ), ); } @@ -514,7 +410,6 @@ class _ChatScreenState extends State { : contact.name, sourceId: widget.contact.publicKeyHex, textScale: textScale, - onTap: () => _openMessagePath(message, contact), onLongPress: () => _showMessageActions(message, contact), onRetryReaction: (msg, emoji) => _sendReaction(msg, contact, emoji), @@ -775,400 +670,32 @@ class _ChatScreenState extends State { ); } - void _showPathHistory(BuildContext context) { - final connector = Provider.of(context, listen: false); - bool showAllPaths = false; - showDialog( + Future _confirmClearChat( + BuildContext context, + MeshCoreConnector connector, + ) async { + final confirmed = await showDialog( context: context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => Consumer( - builder: (context, pathService, _) { - final paths = pathService.getRecentPaths( - widget.contact.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>> - 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: Row( - children: [ - const Icon(Icons.timeline), - const SizedBox(width: 8), - Text(context.l10n.chat_pathManagement), - ], - ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (pathsWithRepeaters.isNotEmpty) ...[ - if (repeatersList.isNotEmpty) - FeatureToggleRow( - title: context.l10n.chat_ShowAllPaths, - subtitle: "", - value: showAllPaths, - onChanged: (val) { - setDialogState(() { - showAllPaths = val; - }); - }, - ), - Text( - context.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.amber[100], - borderRadius: BorderRadius.circular(8), - ), - child: Text( - context.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.hopCount}', - style: const TextStyle(fontSize: 12), - ), - ), - title: Text( - '${path.hopCount} ${path.hopCount == 1 ? context.l10n.chat_hopSingular : context.l10n.chat_hopPlural}', - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(path.timestamp)} • ${path.successCount} ${context.l10n.chat_successes}', - style: const TextStyle(fontSize: 11), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close, size: 16), - tooltip: context.l10n.chat_removePath, - onPressed: () async { - await pathService.removePathRecord( - widget.contact.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( - context.l10n.chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), - ); - return; - } - - final pathBytes = Uint8List.fromList( - path.pathBytes, - ); - // Set the path override to persist user's choice - await connector.setPathOverride( - _resolveContact(connector), - pathLen: path.hopCount, - pathBytes: pathBytes, - ); - - if (!context.mounted) return; - Navigator.pop(context); - await _notifyPathSet( - connector, - _resolveContact(connector), - pathBytes, - path.hopCount, - ); - }, - ), - ); - } - }), - const Divider(), - ] else ...[ - Text(context.l10n.chat_noPathHistoryYet), - const Divider(), - ], - const SizedBox(height: 8), - Text( - context.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( - context.l10n.chat_setCustomPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_setCustomPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () { - Navigator.pop(context); - _showCustomPathDialog(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.orange, - child: Icon(Icons.clear_all, size: 16), - ), - title: Text( - context.l10n.chat_clearPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_clearPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.clearContactPath( - _resolveContact(connector), - ); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(context.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( - context.l10n.chat_forceFloodMode, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_floodModeSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.setPathOverride( - _resolveContact(connector), - pathLen: -1, - ); - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(context.l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ); - Navigator.pop(context); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), - ), - ], - ); - }, - ), - ), - ); - } - - String _formatRelativeTime(DateTime? time) { - if (time == null) return '—'; - final diff = DateTime.now().difference(time); - if (diff.inSeconds < 60) return context.l10n.time_justNow; - if (diff.inMinutes < 60) { - return context.l10n.time_minutesAgo(diff.inMinutes); - } - if (diff.inHours < 24) return context.l10n.time_hoursAgo(diff.inHours); - return context.l10n.time_daysAgo(diff.inDays); - } - - void _showFullPathDialog(BuildContext context, List pathBytes) { - if (pathBytes.isEmpty) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.chat_pathDetailsNotAvailable), - duration: const Duration(seconds: 2), - ); - return; - } - - final connector = context.read(); - 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(context.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, - ), - ), - ], - ), + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.contact_clearChat), 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), + onPressed: () => Navigator.pop(dialogContext, false), + child: Text(context.l10n.common_cancel), ), TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), + onPressed: () => Navigator.pop(dialogContext, true), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + ), + child: Text(context.l10n.common_delete), ), ], ), ); + if (confirmed == true) { + connector.clearMessagesForContact(widget.contact); + } } int _resolveContactIndex = -1; @@ -1213,29 +740,6 @@ class _ChatScreenState extends State { return context.l10n.chat_hopsCount(contact.pathLength); } - Future _notifyPathSet( - MeshCoreConnector connector, - Contact contact, - Uint8List pathBytes, - int hopCount, - ) async { - final verified = connector.isConnected - ? await connector.verifyContactPathOnDevice(contact, pathBytes) - : false; - if (!mounted) return; - - final status = !connector.isConnected - ? context.l10n.chat_pathSavedLocally - : (verified - ? context.l10n.chat_pathDeviceConfirmed - : context.l10n.chat_pathDeviceNotConfirmed); - showDismissibleSnackBar( - context, - content: Text(context.l10n.chat_pathSetHops(hopCount, status)), - duration: const Duration(seconds: 3), - ); - } - void _showContactInfo(BuildContext context) { final connector = Provider.of(context, listen: false); final contact = _resolveContact(connector); @@ -1450,7 +954,12 @@ class _ChatScreenState extends State { children: [ SizedBox( width: 80, - child: Text(label, style: TextStyle(color: Colors.grey[600])), + child: Text( + label, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), Expanded(child: SelectableText(value)), ], @@ -1491,74 +1000,6 @@ class _ChatScreenState extends State { ); } - Future _showCustomPathDialog(BuildContext context) async { - final connector = Provider.of(context, listen: false); - - final currentContact = _resolveContact(connector); - if (currentContact.pathLength > 0 && - currentContact.path.isEmpty && - connector.isConnected) { - connector.getContacts(); - } - - final pathForInput = currentContact.pathFormattedIdList( - connector.pathHashByteWidth, - ); - final currentPathLabel = _currentPathLabel(currentContact); - - // Filter out the current contact from available contacts - final availableContacts = connector.allContacts - .where((c) => c != widget.contact) - .toList(); - - final result = await PathSelectionDialog.show( - context, - availableContacts: availableContacts, - initialPath: pathForInput.isEmpty ? null : pathForInput, - title: context.l10n.chat_setCustomPath, - currentPathLabel: currentPathLabel, - onRefresh: connector.isConnected ? connector.getContacts : null, - pathHashByteWidth: connector.pathHashByteWidth, - ); - - appLogger.info( - 'PathSelectionDialog returned: ${result?.length ?? 0} bytes, mounted: $mounted', - tag: 'ChatScreen', - ); - - if (result == null) { - return; // Cancelled — keep existing path - } - - if (!mounted) { - appLogger.warn( - 'Widget not mounted after dialog, cannot set path', - tag: 'ChatScreen', - ); - return; - } - - appLogger.info( - 'Calling setPathOverride for ${widget.contact.name}', - tag: 'ChatScreen', - ); - final hopsCount = result.length ~/ connector.pathHashByteWidth; - await connector.setPathOverride( - _resolveContact(connector), - pathLen: hopsCount, - pathBytes: result, - ); - appLogger.info('setPathOverride completed', tag: 'ChatScreen'); - - if (!mounted) return; - await _notifyPathSet( - connector, - _resolveContact(connector), - result, - hopsCount, - ); - } - void _openMessagePath(Message message, Contact contact) { final connector = context.read(); final fourByteHex = message.fourByteRoomContactKey @@ -1618,15 +1059,14 @@ class _ChatScreenState extends State { _showEmojiPicker(message, contact); }, ), - if (PlatformInfo.isDesktop) - ListTile( - leading: const Icon(Icons.route), - title: Text(context.l10n.chat_path), - onTap: () { - Navigator.pop(sheetContext); - _openMessagePath(message, contact); - }, - ), + ListTile( + leading: const Icon(Icons.route), + title: Text(context.l10n.chat_path), + onTap: () { + Navigator.pop(sheetContext); + _openMessagePath(message, contact); + }, + ), ListTile( leading: const Icon(Icons.copy), title: Text(context.l10n.common_copy), @@ -1659,14 +1099,6 @@ class _ChatScreenState extends State { _markAsUnread(message); }, ), - ListTile( - leading: const Icon(Icons.delete_outline), - title: Text(context.l10n.common_delete), - onTap: () async { - Navigator.pop(sheetContext); - await _deleteMessage(message); - }, - ), if (message.isOutgoing && message.status == MessageStatus.failed) ListTile( leading: const Icon(Icons.refresh), @@ -1686,10 +1118,20 @@ class _ChatScreenState extends State { _openChat(context, contact); }, ), + const Divider(), ListTile( - leading: const Icon(Icons.close), - title: Text(context.l10n.common_cancel), - onTap: () => Navigator.pop(sheetContext), + 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); + }, ), ], ), @@ -1716,8 +1158,7 @@ class _ChatScreenState extends State { void _retryMessage(Message message) { final connector = Provider.of(context, listen: false); - // Retry using the contact's current path override setting - connector.sendMessage(_resolveContact(connector), message.text); + connector.resendMessage(_resolveContact(connector), message); showDismissibleSnackBar( context, content: Text(context.l10n.chat_retryingMessage), @@ -1761,7 +1202,6 @@ class _ChatScreenState extends State { class _MessageBubble extends StatelessWidget { final Message message; final String senderName; - final VoidCallback? onTap; final VoidCallback? onLongPress; final void Function(Message message, String emoji)? onRetryReaction; final double textScale; @@ -1772,7 +1212,6 @@ class _MessageBubble extends StatelessWidget { required this.senderName, required this.sourceId, required this.textScale, - this.onTap, this.onLongPress, this.onRetryReaction, }); @@ -1814,160 +1253,112 @@ class _MessageBubble extends StatelessWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: PlatformInfo.isDesktop ? null : onTap, - onLongPress: onLongPress, - onSecondaryTapUp: PlatformInfo.isDesktop - ? (_) => onLongPress?.call() - : null, - child: Row( - mainAxisAlignment: isOutgoing - ? MainAxisAlignment.end - : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - _buildAvatar(senderName, colorScheme), - const SizedBox(width: 8), - ], - Flexible( - child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, - ), - decoration: BoxDecoration( - color: bubbleColor, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - top: 4, - bottom: 4, - ) - : EdgeInsets.zero, - child: Text( - senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.primary, + LayoutBuilder( + builder: (context, constraints) => GestureDetector( + onLongPress: onLongPress, + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => onLongPress?.call() + : null, + child: Row( + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + _buildAvatar(senderName, colorScheme), + const SizedBox(width: 8), + ], + Flexible( + child: Container( + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.65, + ), + decoration: BoxDecoration( + color: bubbleColor, + borderRadius: BorderRadius.circular(16), + border: isFailed + ? Border.all(color: colorScheme.error) + : null, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), ), ), - ), - if (gifId == null) const SizedBox(height: 4), - ], - if (poi != null) - _buildPoiMessage( - context, - poi, - textColor, - metaColor, - textScale, - senderName, - trailing: (!enableTracing && isOutgoing) - ? Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty, - isFailed: - message.status == - MessageStatus.failed, + if (gifId == null) const SizedBox(height: 4), + ], + if (poi != null) + _buildPoiMessage( + context, + poi, + textColor, + metaColor, + textScale, + senderName, + ) + else if (gifId != null) + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues( + alpha: 0.7, ), - ) - : null, - ) - else if (gifId != null) - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: textColor.withValues( - alpha: 0.7, - ), - ), - ), - if (!enableTracing && isOutgoing) - Positioned( - top: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - color: bubbleColor, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(10), - topRight: Radius.circular(12), - ), - ), - child: MessageStatusIcon( - isAcked: - message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty, - isFailed: - message.status == - MessageStatus.failed, - ), - ), - ), - ], - ) - else - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible( - child: TranslatedMessageContent( - displayText: translatedDisplayText, - originalText: originalDisplayText, - style: TextStyle( - color: textColor, - fontSize: bodyFontSize * textScale, - ), - originalStyle: TextStyle( - color: textColor.withValues(alpha: 0.78), - fontSize: bodyFontSize * textScale, - ), - ), - ), - if (!enableTracing && isOutgoing) ...[ - const SizedBox(width: 4), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty, - isFailed: - message.status == MessageStatus.failed, ), ), ], - ], - ), - if (enableTracing) ...[ - if (isOutgoing && message.retryCount > 0) ...[ + ) + else + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: TranslatedMessageContent( + displayText: translatedDisplayText, + originalText: originalDisplayText, + style: TextStyle( + color: textColor, + fontSize: bodyFontSize * textScale, + ), + originalStyle: TextStyle( + color: textColor.withValues(alpha: 0.78), + fontSize: bodyFontSize * textScale, + ), + ), + ), + ], + ), + if (enableTracing && + isOutgoing && + message.retryCount > 0) ...[ const SizedBox(height: 4), Padding( padding: gifId != null @@ -1982,7 +1373,7 @@ class _MessageBubble extends StatelessWidget { .maxMessageRetries, ), style: TextStyle( - fontSize: 10, + fontSize: 10 * textScale, color: metaColor, fontWeight: FontWeight.w500, ), @@ -2005,32 +1396,47 @@ class _MessageBubble extends StatelessWidget { Text( _formatTime(message.timestamp), style: TextStyle( - fontSize: 10, + fontSize: 10 * textScale, color: metaColor, ), ), if (isOutgoing) ...[ const SizedBox(width: 4), - _buildStatusIcon(metaColor), + MessageStatusIcon( + size: 12 * textScale, + onColor: metaColor, + isAcked: + message.status == + MessageStatus.delivered, + isPending: + message.status == MessageStatus.pending, + isFailed: + message.status == MessageStatus.failed, + ), ], - if (message.tripTimeMs != null && + if (enableTracing && + message.tripTimeMs != null && message.status == MessageStatus.delivered) ...[ const SizedBox(width: 4), Icon( Icons.speed, - size: 10, + size: 10 * textScale, color: isOutgoing ? metaColor - : Colors.green[700], + : Theme.of( + context, + ).colorScheme.tertiary, ), Text( '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', style: TextStyle( - fontSize: 9, + fontSize: 9 * textScale, color: isOutgoing ? metaColor - : Colors.green[700], + : Theme.of( + context, + ).colorScheme.tertiary, ), ), ], @@ -2038,11 +1444,11 @@ class _MessageBubble extends StatelessWidget { ), ), ], - ], + ), ), ), - ), - ], + ], + ), ), ), if (message.reactions.isNotEmpty) ...[ @@ -2072,9 +1478,11 @@ class _MessageBubble extends StatelessWidget { IconButton( icon: Icon(Icons.location_on_outlined, color: textColor), padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), onPressed: () async { - final selfName = context.read().selfName ?? 'Me'; + final selfName = + context.read().selfName ?? + context.l10n.chat_me; final fromName = message.isOutgoing ? selfName : senderName; final key = buildSharedMarkerKey( sourceId: sourceId, @@ -2197,8 +1605,8 @@ class _MessageBubble extends StatelessWidget { } Widget _buildAvatar(String senderName, ColorScheme colorScheme) { - final initial = _getFirstCharacterOrEmoji(senderName); - final color = _getColorForName(senderName); + final initial = firstCharacterOrEmoji(senderName); + final color = colorForName(senderName); return CircleAvatar( radius: 18, @@ -2214,56 +1622,6 @@ class _MessageBubble extends StatelessWidget { ); } - 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 _buildStatusIcon(Color color) { - IconData icon; - switch (message.status) { - case MessageStatus.pending: - icon = Icons.access_time; - break; - case MessageStatus.sent: - icon = Icons.schedule; - break; - case MessageStatus.delivered: - icon = Icons.check; - break; - case MessageStatus.failed: - icon = Icons.error_outline; - break; - } - - return Icon(icon, size: 12, color: color); - } - String _formatTime(DateTime time) { final hour = time.hour.toString().padLeft(2, '0'); final minute = time.minute.toString().padLeft(2, '0'); diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart index 1827aeb9..cdf3c938 100644 --- a/lib/screens/chrome_required_screen.dart +++ b/lib/screens/chrome_required_screen.dart @@ -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, ), ), diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index 9c376769..f666254c 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -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)); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 2ebe3a23..fedc428a 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -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 String _loadedGroupScopeKeyHex = ''; Timer? _searchDebounce; - final Set _pendingOperations = {}; + final List _pendingOperations = []; StreamSubscription? _frameSubscription; @@ -185,59 +186,52 @@ class _ContactsScreenState extends State 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)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_contactImported), - ); + if (_pendingOperations.isEmpty) return; + final op = _pendingOperations.removeAt(0); + switch (op) { + case ContactOperationType.import: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImported), + ); + case ContactOperationType.zeroHopShare: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertSent), + ); + case ContactOperationType.export: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopied), + ); } - - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_zeroHopContactAdvertSent), - ); - } - - if (_pendingOperations.contains(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)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_contactImportFailed), - ); + if (_pendingOperations.isEmpty) return; + final op = _pendingOperations.removeAt(0); + switch (op) { + case ContactOperationType.import: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImportFailed), + ); + case ContactOperationType.zeroHopShare: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), + ); + case ContactOperationType.export: + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopyFailed), + ); } - - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), - ); - } - if (_pendingOperations.contains(ContactOperationType.export)) { - showDismissibleSnackBar( - context, - content: Text(context.l10n.contacts_contactAdvertCopyFailed), - ); - } - - _pendingOperations.clear(); } } catch (e) { appLogger.error( @@ -252,17 +246,37 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); _pendingOperations.add(ContactOperationType.export); - await connector.sendFrame(exportContactFrame, expectsGenericAck: true); + try { + await connector.sendFrame(exportContactFrame, expectsGenericAck: true); + } catch (e) { + _pendingOperations.remove(ContactOperationType.export); + if (mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopyFailed), + ); + } + } } Future _contactZeroHop(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactZeroHopFrame = buildZeroHopContact(pubKey); _pendingOperations.add(ContactOperationType.zeroHopShare); - await connector.sendFrame( - exportContactZeroHopFrame, - expectsGenericAck: true, - ); + try { + await connector.sendFrame( + exportContactZeroHopFrame, + expectsGenericAck: true, + ); + } catch (e) { + _pendingOperations.remove(ContactOperationType.zeroHopShare); + if (mounted) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), + ); + } + } } Future _contactImport() async { @@ -288,11 +302,10 @@ class _ContactsScreenState extends State 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 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 bottom: const SyncProgressAppBarBottom(), actions: [ PopupMenuButton( - itemBuilder: (context) => [ + tooltip: context.l10n.contacts_moreOptions, + itemBuilder: (context) => >[ + 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 ), onTap: () => _contactExport(Uint8List.fromList([])), ), + const PopupMenuDivider(), PopupMenuItem( child: Row( children: [ - const Icon(Icons.paste), - const SizedBox(width: 8), - Text(context.l10n.contacts_addContactFromClipboard), - ], - ), - onTap: () => _contactImport(), - ), - ], - icon: const Icon(Icons.connect_without_contact), - ), - 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), ], ), 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 ], ), 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 ); } + 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 _disconnect( BuildContext context, MeshCoreConnector connector, @@ -571,7 +636,11 @@ class _ContactsScreenState extends State 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,16 +658,25 @@ class _ContactsScreenState extends State ], child: SizedBox( height: 48, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - children: [ - Expanded( - child: Text(selectedGroupName, overflow: TextOverflow.ellipsis), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down), - ], + 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, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_drop_down), + ], + ), ), ), ), @@ -624,6 +702,14 @@ class _ContactsScreenState extends State 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 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 ), ), Expanded( - child: filteredAndSorted.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - 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( + child: RefreshIndicator( + onRefresh: () => connector.getContacts(), + child: filteredAndSorted.isEmpty + ? LayoutBuilder( + builder: (context, constraints) => ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: EmptyState( + icon: Icons.search_off, + title: viewState.contactsShowUnreadOnly + ? context.l10n.contacts_noUnreadContacts + : context.l10n.contacts_noContactsFound, + ), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.only(bottom: 88), itemCount: filteredAndSorted.length, itemBuilder: (context, index) { final contact = filteredAndSorted[index]; @@ -827,7 +920,7 @@ class _ContactsScreenState extends State ); }, ), - ), + ), ), ], ); @@ -1048,7 +1141,7 @@ class _ContactsScreenState extends State }, 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 ); }, ), - 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 }, ), 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 }, child: Text( context.l10n.common_delete, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], @@ -1486,25 +1574,14 @@ 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( - contact.pathLabel(context.l10n), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - contact.shortPubKeyHex, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12), - ), - ], + subtitle: Text( + contact.pathLabel(context.l10n), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. @@ -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) { diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index f9f0e07e..1bc392bf 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -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 { 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 { 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 { 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 { 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 { 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 { ), 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 { 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 { 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 { } } - 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); diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index f908f5ea..af80ca22 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -73,7 +73,7 @@ class _LineOfSightMapScreenState extends State { 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 { 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 { setState(() { _result = result; _selectedObstruction = _defaultObstructionFor(result); + _menuExpanded = true; }); } catch (e) { if (!mounted) return; @@ -506,7 +507,9 @@ class _LineOfSightMapScreenState extends State { 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 { ), 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 { 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 { '${_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 { 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 { '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 { 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 { ), 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, ); diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 4057e0ec..e627bd58 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -83,17 +83,17 @@ class _MapCacheScreenState extends State { 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 { 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, + ), ), ), ], diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 92decf22..044f2183 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -185,17 +185,17 @@ class _MapScreenState extends State { 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,61 +420,79 @@ class _MapScreenState extends State { automaticallyImplyLeading: false, bottom: const SyncProgressAppBarBottom(), actions: [ - if (!_isBuildingPathTrace) - IconButton( - icon: const Icon(Icons.radar), - onPressed: () => _startPath( - LatLng(connector.selfLatitude!, connector.selfLongitude!), - ), - tooltip: context.l10n.contacts_pathTrace, - ), - if (!_isBuildingPathTrace) - IconButton( - icon: const LosIcon(), - onPressed: () { - final candidates = []; - if (connector.selfLatitude != null && - connector.selfLongitude != null) { - candidates.add( - LineOfSightEndpoint( - label: context.l10n.pathTrace_you, - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - color: Colors.teal, - icon: Icons.person_pin_circle, - ), - ); - } - for (final c in contactsWithLocation) { - candidates.add( - LineOfSightEndpoint( - label: c.name, - point: LatLng(c.latitude!, c.longitude!), - color: _getNodeColor(c.type), - icon: _getNodeIcon(c.type), - ), - ); - } - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LineOfSightMapScreen( - title: context.l10n.map_losScreenTitle, - candidates: candidates, - ), - ), - ); - }, - tooltip: context.l10n.map_lineOfSight, - ), 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!, + ), + ), + ), + if (!_isBuildingPathTrace) + PopupMenuItem( + child: Row( + children: [ + const LosIcon(), + const SizedBox(width: 8), + Text(context.l10n.map_lineOfSight), + ], + ), + onTap: () { + final candidates = []; + if (connector.selfLatitude != null && + connector.selfLongitude != null) { + candidates.add( + LineOfSightEndpoint( + label: context.l10n.pathTrace_you, + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + color: Colors.teal, + icon: Icons.person_pin_circle, + ), + ); + } + for (final c in contactsWithLocation) { + candidates.add( + LineOfSightEndpoint( + label: c.name, + point: LatLng(c.latitude!, c.longitude!), + color: _getNodeColor(c.type), + icon: _getNodeIcon(c.type), + ), + ); + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LineOfSightMapScreen( + title: context.l10n.map_losScreenTitle, + candidates: candidates, + ), + ), + ); + }, + ), 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 { 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,26 +938,28 @@ class _MapScreenState extends State { guess.contact, guessedPosition: guess.position, ), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: color.withValues( - alpha: guess.highConfidence ? 0.55 : 0.30, - ), - 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), + child: Center( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color.withValues( + alpha: guess.highConfidence ? 0.55 : 0.30, ), - ], - ), - child: const Icon( - Icons.not_listed_location, - color: Colors.white, - size: 20, + 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), + ), + ], + ), + child: const Icon( + Icons.not_listed_location, + color: Colors.white, + size: 20, + ), ), ), ), @@ -1041,39 +1061,37 @@ class _MapScreenState extends State { 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( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: settings.mapShowOverlaps && !_isBuildingPathTrace - ? Colors.red - : _getNodeColor(contact.type), - 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), - ), - ], - ), - child: Icon( - _getNodeIcon(contact.type), - color: Colors.white, - size: 20, - ), + child: Center( + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: settings.mapShowOverlaps && !_isBuildingPathTrace + ? Colors.red + : _getNodeColor(contact.type), + 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), + ), + ], ), - ], + child: Icon( + _getNodeIcon(contact.type), + color: Colors.white, + size: 20, + ), + ), ), ), ); @@ -1219,7 +1237,9 @@ class _MapScreenState extends State { 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 { ), 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 { ), Row( children: [ - const Icon( + Icon( Icons.add_outlined, size: 16, - color: Colors.grey, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), Text( ": $nodeCountAll", @@ -1547,56 +1571,14 @@ class _MapScreenState extends State { LatLng? guessedPosition, }) { final connector = context.read(); - showDialog( + showModalBottomSheet( context: context, - builder: (dialogContext) => AlertDialog( - title: Row( - children: [ - Icon( - _getNodeIcon(contact.type), - color: _getNodeColor(contact.type), - ), - const SizedBox(width: 8), - Expanded(child: SelectableText(contact.name)), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow( - context.l10n.map_type, - contact.typeLabel(context.l10n), - ), - _buildInfoRow( - context.l10n.map_path, - contact.pathLabel(context.l10n), - ), - if (contact.hasLocation) - _buildInfoRow( - context.l10n.map_location, - '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', - ) - else if (guessedPosition != null) - _buildInfoRow( - context.l10n.map_estLocation, - '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', - ), - _buildInfoRow( - context.l10n.map_lastSeen, - _formatLastSeen(contact.lastSeen), - ), - _buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text(context.l10n.common_close), - ), - if (contact.type == - advTypeChat) // Only show chat button for chat nodes - TextButton( + showDragHandle: true, + builder: (sheetContext) { + final actions = []; + if (contact.type == advTypeChat) { + actions.add( + FilledButton( onPressed: () { if (!contact.isActive) { connector.importDiscoveredContact(contact); @@ -1604,7 +1586,7 @@ class _MapScreenState extends State { final unread = connector.getUnreadCountForContactKey( contact.publicKeyHex, ); - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); Navigator.push( context, MaterialPageRoute( @@ -1617,30 +1599,93 @@ class _MapScreenState extends State { }, child: Text(context.l10n.contacts_openChat), ), - if (contact.type == advTypeRepeater) - TextButton( + ); + } + if (contact.type == advTypeRepeater) { + actions.add( + FilledButton( onPressed: () { if (!contact.isActive) { connector.importDiscoveredContact(contact); } - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); _showRepeaterLogin(context, contact); }, child: Text(context.l10n.map_manageRepeater), ), - if (contact.type == advTypeRoom) - TextButton( + ); + } + if (contact.type == advTypeRoom) { + actions.add( + FilledButton( onPressed: () { if (!contact.isActive) { connector.importDiscoveredContact(contact); } - Navigator.pop(dialogContext); + 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), + color: _getNodeColor(contact.type), + ), + const SizedBox(width: 8), + Expanded(child: SelectableText(contact.name)), + ], + ), + const SizedBox(height: 8), + _buildInfoRow( + context.l10n.map_type, + contact.typeLabel(context.l10n), + ), + _buildInfoRow( + context.l10n.map_path, + contact.pathLabel(context.l10n), + ), + if (contact.hasLocation) + _buildInfoRow( + context.l10n.map_location, + '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', + ) + else if (guessedPosition != null) + _buildInfoRow( + context.l10n.map_estLocation, + '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + ), + _buildInfoRow( + context.l10n.map_lastSeen, + _formatLastSeen(contact.lastSeen), + ), + _buildInfoRow( + context.l10n.map_publicKey, + contact.publicKeyHex, + ), + const SizedBox(height: 16), + ...actions, + TextButton( + onPressed: () => Navigator.pop(sheetContext), + child: Text(context.l10n.common_close), + ), + ], + ), + ), + ), + ); + }, ); } @@ -1725,6 +1770,9 @@ class _MapScreenState extends State { 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 { 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 { ); 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 { 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 { 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( diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 77559d48..c34af7f1 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -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 { 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 { 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 { 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 { 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 { fontSize: 14, fontWeight: FontWeight.normal, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), centerTitle: false, actions: [ - PopupMenuButton( + 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 { 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 { fmtDuration(entry.value['lastHeard'] + 0.0), ), entry.value['snr'], - connector.currentSf!, + connector.currentSf, ), ], ), @@ -447,7 +388,7 @@ class _NeighborsScreenState extends State { String label, String value, double snr, - int spreadingFactor, + int? spreadingFactor, ) { final snrUi = snrUiFromSNR(snr, spreadingFactor); return Padding( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 7eba4b0c..a7042cf7 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -119,6 +119,9 @@ class _PathTraceMapScreenState extends State { 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 { 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 { 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 _doPathTrace() async { if (mounted) { setState(() { @@ -336,9 +356,12 @@ class _PathTraceMapScreenState extends State { final connector = Provider.of(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 { } 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 snrData = buffer - .readRemainingBytes() + .readBytes(snrCount) .map((snr) => snr.toSigned(8).toDouble() / 4) .toList(); @@ -559,13 +584,20 @@ class _PathTraceMapScreenState extends State { 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 { !_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,31 +829,35 @@ class _PathTraceMapScreenState extends State { markers.add( Marker( point: point, - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: hasGps - ? Colors.green - : Colors.orange.withValues(alpha: 0.75), - 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), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: hasGps + ? Colors.green + : Colors.orange.withValues(alpha: 0.75), + 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: Text( + hasGps ? shortLabel : '~$shortLabel', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - hasGps ? shortLabel : '~$shortLabel', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -839,29 +877,33 @@ class _PathTraceMapScreenState extends State { markers.add( Marker( point: selfPoint, - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.blue, - 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), + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.blue, + 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: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -885,26 +927,30 @@ class _PathTraceMapScreenState extends State { markers.add( Marker( point: targetPos, - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: isGuessed - ? Colors.purple.withValues(alpha: 0.55) - : Colors.red, - 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), - ), - ], + width: 48, + height: 48, + child: Center( + child: Container( + width: 35, + height: 35, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: isGuessed + ? Colors.purple.withValues(alpha: 0.55) + : Colors.red, + 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: const Icon(Icons.person, color: Colors.white, size: 18), ), - alignment: Alignment.center, - child: const Icon(Icons.person, color: Colors.white, size: 18), ), ), ); @@ -1077,6 +1123,12 @@ class _PathTraceMapScreenState extends State { ); } + Widget _colorDot(Color color) => Container( + width: 10, + height: 10, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ); + Widget _buildLegendCard( BuildContext context, PathTraceData pathTraceData, @@ -1099,9 +1151,32 @@ class _PathTraceMapScreenState extends State { children: [ Padding( padding: const EdgeInsets.all(12), - child: Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}', - style: const TextStyle(fontWeight: FontWeight.w600), + 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), diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 10e325ac..80269061 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -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 { 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( + 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 { tooltip: l10n.repeater_clearHistory, onPressed: _commandHistory.isEmpty ? null : _clearHistory, ), + PopupMenuButton( + 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 { 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, + ), ), ], ), diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 5b1e30ca..2aebcf03 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -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, + ), ], ), ), diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 9ffd1b37..e04cfb70 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -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 { // 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 { ? 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 { : 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 { } 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 { 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 { content: Text( l10n.repeater_settingsPartialFailure(failures.join('; ')), ), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -973,7 +978,7 @@ class _RepeaterSettingsScreenState extends State { 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 { _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 { ), centerTitle: false, actions: [ - PopupMenuButton( + 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 { const SizedBox(height: 16), _buildAdvancedCard(), const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 16), _buildDangerZoneCard(), ], ), @@ -1388,13 +1339,22 @@ class _RepeaterSettingsScreenState extends State { 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 { 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 { 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 { onConfirm(); }, style: isDestructive - ? FilledButton.styleFrom(backgroundColor: Colors.red) + ? FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + ) : null, child: Text(l10n.repeater_confirm), ), diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 9dbd8e31..f121605d 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -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 { 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 { 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 { 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( + 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 { 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( - value, - style: const TextStyle(fontWeight: FontWeight.w400), - ), + const SizedBox(width: 8), + Text( + value, + style: const TextStyle(fontWeight: FontWeight.w400), + textAlign: TextAlign.end, ), ], ), diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index ec148c5f..1fd2c53c 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -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 { 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 { 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,84 +150,26 @@ class _ScannerScreenState extends State { }, ), ), - bottomNavigationBar: Consumer( + floatingActionButton: Consumer( 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( - heroTag: 'scanner_ble_action', - onPressed: isBluetoothOff - ? null - : () { - if (isScanning) { - connector.stopScan(); - } else { - unawaited( - connector.startScan().catchError((e) { - appLogger.warn( - 'startScan error: $e', - tag: 'ScannerScreen', - ); - }), - ); - } - }, - icon: isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.bluetooth_searching), - label: Text( - isScanning - ? context.l10n.scanner_stop - : context.l10n.scanner_scan, - ), - ), - ], - ), + return FloatingActionButton.extended( + heroTag: 'scanner_ble_action', + onPressed: isBluetoothOff ? null : () => _toggleScan(connector), + icon: isScanning + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.bluetooth_searching), + label: Text( + isScanning + ? context.l10n.scanner_stop + : context.l10n.scanner_scan, ), ); }, @@ -207,6 +177,18 @@ class _ScannerScreenState extends State { ); } + 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 { 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 - ? context.l10n.scanner_searchingDevices - : context.l10n.scanner_tapToScan, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - ], - ), + 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, + 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 { 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 { 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; + }); + } } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index eccf9bb8..9208853b 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -53,6 +53,7 @@ class _SettingsScreenState extends State { Future _loadVersionInfo() async { final packageInfo = await PackageInfo.fromPlatform(); + if (!mounted) return; setState(() { _appVersion = packageInfo.version; }); @@ -213,12 +214,12 @@ class _SettingsScreenState extends State { 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 { 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 { ), ), 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 { ), 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 { 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 { 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 { ), TextButton( onPressed: () async { - Navigator.pop(context); - + int? interval; if (hasGPS) { final intervalText = intervalController.text.trim(); - if (intervalText.isEmpty) { - return; + 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 { ); } + 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 { }, 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(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), + ), ], ); } diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index a0d71922..517b39a6 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -95,12 +95,14 @@ class _TcpScreenState extends State { 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,40 +156,32 @@ class _TcpScreenState extends State { }, ), ), - bottomNavigationBar: SafeArea( - top: false, - minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (PlatformInfo.supportsUsbSerial) - FloatingActionButton.extended( - 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), - icon: const Icon(Icons.bluetooth), - label: Text(context.l10n.connectionChoiceBluetoothLabel), - ), - ], + ); + } + + Widget _buildTransportLinks(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Wrap( + spacing: 12, + runSpacing: 8, + children: [ + if (PlatformInfo.supportsUsbSerial) + OutlinedButton.icon( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const UsbScreen()), + ); + }, + icon: const Icon(Icons.usb), + label: Text(context.l10n.connectionChoiceUsbLabel), + ), + 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 { 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 { 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, - ), - ), + child: Text( + statusText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), ), ), ], @@ -274,7 +263,7 @@ class _TcpScreenState extends State { showDismissibleSnackBar( context, content: Text(message), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 873ca3b1..4559adac 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -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 { + 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 { RepeaterCommandService? _commandService; PathSelection? _pendingStatusSelection; List>? _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 { super.initState(); final connector = Provider.of(context, listen: false); _commandService = RepeaterCommandService(connector); + _loadAutoRefreshSettings(); _setupMessageListener(); _loadTelemetry(); _hasData = false; @@ -82,17 +104,26 @@ class _TelemetryScreenState extends State { _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; + } }); - showDismissibleSnackBar( - context, - content: Text(context.l10n.telemetry_requestTimeout), - backgroundColor: Colors.red, - ); + if (!isAutoRefreshRequest) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_requestTimeout), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } + if (isAutoRefreshRequest && _isAutoRefreshEnabled) { + _scheduleNextAutoRefreshAttempt(); + } _recordTelemetryResult(false); }); } @@ -134,15 +165,21 @@ class _TelemetryScreenState extends State { ); } if (!mounted) return; + final isAutoRefreshRequest = _activeTelemetryRequestIsAutoRefresh; setState(() { _parsedTelemetry = parsedTelemetry; + if (isAutoRefreshRequest) { + _autoRefreshLastAttemptFailed = false; + } + _activeTelemetryRequestIsAutoRefresh = false; }); - showDismissibleSnackBar( - context, - content: Text(context.l10n.telemetry_receivedData), - backgroundColor: Colors.green, - ); + if (!isAutoRefreshRequest) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_receivedData), + ); + } _statusTimeout?.cancel(); if (!mounted) return; setState(() { @@ -150,14 +187,18 @@ class _TelemetryScreenState extends State { _isLoaded = true; _hasData = true; }); + if (isAutoRefreshRequest) { + _scheduleNextAutoRefreshAttempt(); + } } - Future _loadTelemetry() async { + Future _loadTelemetry({bool isAutoRefresh = false}) async { if (_commandService == null) return; setState(() { _isLoading = true; _isLoaded = false; + _activeTelemetryRequestIsAutoRefresh = isAutoRefresh; }); try { final connector = Provider.of(context, listen: false); @@ -169,7 +210,7 @@ class _TelemetryScreenState extends State { if (widget.contact.type != advTypeChat) { frame = buildSendBinaryReq( widget.contact.publicKey, - payload: Uint8List.fromList([reqTypeGetTelemetry]), + payload: buildTelemetryBinaryPayload(), ); } else { frame = buildSendTelemetryReq(widget.contact.publicKey); @@ -180,17 +221,76 @@ class _TelemetryScreenState extends State { setState(() { _isLoading = false; _isLoaded = false; + if (isAutoRefresh) { + _autoRefreshLastAttemptFailed = true; + } + _activeTelemetryRequestIsAutoRefresh = false; }); + if (isAutoRefresh) { + _scheduleNextAutoRefreshAttempt(); + } - showDismissibleSnackBar( - context, - content: Text(context.l10n.telemetry_errorLoading(e.toString())), - backgroundColor: Colors.red, - ); + if (!isAutoRefresh) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_errorLoading(e.toString())), + 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 _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; if (selection == null) return; @@ -206,9 +306,13 @@ class _TelemetryScreenState extends State { @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 { final connector = context.watch(); final settings = context.watch().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 { centerTitle: false, bottom: const SyncProgressAppBarBottom(), actions: [ - PopupMenuButton( + 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 { 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 { 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 { 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 { entry['channel'], isImperialUnits, ), + _buildAutoRefreshCard(), ], ), ), @@ -359,7 +415,6 @@ class _TelemetryScreenState extends State { 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 { ), 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 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 _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( - value, - style: const TextStyle(fontWeight: FontWeight.w400), - ), + const SizedBox(width: 8), + Text( + value, + style: const TextStyle(fontWeight: FontWeight.w400), + textAlign: TextAlign.end, ), ], ), @@ -485,3 +979,10 @@ class _TelemetryScreenState extends State { return '${tempC.toStringAsFixed(1)}°C'; } } + +class _TelemetryFieldDisplay { + final String label; + final String value; + + const _TelemetryFieldDisplay(this.label, this.value); +} diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 25992de8..ec551c6f 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -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,81 +99,62 @@ class _UsbScreenState extends State { return Column( children: [ _buildStatusBar(context, connector), + _buildTransportLinks(context), Expanded(child: _buildPortList(context, connector)), ], ); }, ), ), - bottomNavigationBar: Consumer( - builder: (context, connector, child) { - final isLoading = _isLoadingPorts; - final showBle = true; - final showTcp = !PlatformInfo.isWeb; - - return SafeArea( - top: false, - minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, + bottomNavigationBar: _supportsHotPlug + ? null + : SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), 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, - heroTag: 'usb_refresh_action', - extendedPadding: const EdgeInsets.symmetric( - horizontal: 12, - ), - icon: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.usb), - label: Text(context.l10n.scanner_scan), - ), + FloatingActionButton.extended( + onPressed: _isLoadingPorts ? null : _loadPorts, + heroTag: 'usb_refresh_action', + icon: _isLoadingPorts + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.usb), + label: Text(context.l10n.scanner_scan), + ), ], ), ), - ); - }, + ); + } + + 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 { 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 { 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 { 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 { 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, - ), - ), + child: Text( + statusText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), ), ), ], @@ -244,11 +219,18 @@ class _UsbScreenState extends State { 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 { 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 { 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 { showDismissibleSnackBar( context, content: Text(_friendlyErrorMessage(error)), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 733dfc59..a15b9c7d 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -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( diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index c0f30153..6e493337 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -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, diff --git a/lib/services/translation_service.dart b/lib/services/translation_service.dart index 7b1d7f5f..d01155ab 100644 --- a/lib/services/translation_service.dart +++ b/lib/services/translation_service.dart @@ -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 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(); _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; diff --git a/lib/theme/mesh_theme.dart b/lib/theme/mesh_theme.dart index 7c35d57c..00b92ec0 100644 --- a/lib/theme/mesh_theme.dart +++ b/lib/theme/mesh_theme.dart @@ -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); } diff --git a/lib/widgets/byte_count_input.dart b/lib/widgets/byte_count_input.dart index ca432522..91f638b8 100644 --- a/lib/widgets/byte_count_input.dart +++ b/lib/widgets/byte_count_input.dart @@ -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 { ), ), ), + ), ], ); }, diff --git a/lib/widgets/device_tile.dart b/lib/widgets/device_tile.dart index 3f30b38e..9012128c 100644 --- a/lib/widgets/device_tile.dart +++ b/lib/widgets/device_tile.dart @@ -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, ); } diff --git a/lib/widgets/empty_state.dart b/lib/widgets/empty_state.dart index 172c9a4e..718c1c44 100644 --- a/lib/widgets/empty_state.dart +++ b/lib/widgets/empty_state.dart @@ -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, ), ], diff --git a/lib/widgets/gif_picker.dart b/lib/widgets/gif_picker.dart index 9c569512..90b6261b 100644 --- a/lib/widgets/gif_picker.dart +++ b/lib/widgets/gif_picker.dart @@ -180,7 +180,10 @@ class _GifPickerState extends State { 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 { 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 { 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, + ), ), ], ), diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart index 0689f0b5..cf9cd7d6 100644 --- a/lib/widgets/message_status_icon.dart +++ b/lib/widgets/message_status_icon.dart @@ -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 createState() => _MessageStatusIconState(); +} + +class _MessageStatusIconState extends State + 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) { + 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), + ); + } + + if (widget.isPending) { + return Semantics( + label: l10n.messageStatus_pending, + child: _SendingDots( + controller: _controller, + color: baseColor, + size: size, + ), + ); + } + + 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) { - if (isFailed) { - return Icon(Icons.cancel, size: size, color: Colors.red); - } - - final Color color; - if (isAcked) { - color = Colors.green; - } else { - color = Colors.grey; - } - - return SvgPicture.asset( - 'assets/icons/done_all.svg', - width: size, + final double dot = (size * 0.24).clamp(2.0, 4.0); + return SizedBox( height: size, - colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + 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, + ), + ), + ); + }), + ); + }, + ), ); } } diff --git a/lib/widgets/path_editor_sheet.dart b/lib/widgets/path_editor_sheet.dart new file mode 100644 index 00000000..67d9510c --- /dev/null +++ b/lib/widgets/path_editor_sheet.dart @@ -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 availableContacts; + final List initialPath; + final int pathHashByteWidth; + + const PathEditorSheet({ + super.key, + required this.availableContacts, + this.initialPath = const [], + this.pathHashByteWidth = pathHashSize, + }); + + static Future show( + BuildContext context, { + required List availableContacts, + List initialPath = const [], + int pathHashByteWidth = pathHashSize, + }) { + return showModalBottomSheet( + 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 createState() => _PathEditorSheetState(); +} + +class _Hop { + final int id; + final Uint8List bytes; + + const _Hop(this.id, this.bytes); +} + +class _PathEditorSheetState extends State { + 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 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 = []; + 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 = []; + 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), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart deleted file mode 100644 index ca8b2c92..00000000 --- a/lib/widgets/path_management_dialog.dart +++ /dev/null @@ -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 show(BuildContext context, {required Contact contact}) { - return showDialog( - 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 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(); - 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 _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( - 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>> 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), - ), - ], - ); - }, - ); - } -} diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart deleted file mode 100644 index c87b5a9b..00000000 --- a/lib/widgets/path_selection_dialog.dart +++ /dev/null @@ -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 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 createState() => _PathSelectionDialogState(); - - static Future show( - BuildContext context, { - required List availableContacts, - String? title, - String? initialPath, - String? currentPathLabel, - VoidCallback? onRefresh, - int pathHashByteWidth = 1, - }) { - return showDialog( - 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 { - late TextEditingController _controller; - final List _selectedContacts = []; - List _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 _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 = []; - final invalidPrefixes = []; - 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), - ), - ], - ); - } -} diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index 40dcb59a..bcb4781f 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -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); } } diff --git a/lib/widgets/radio_stats_entry.dart b/lib/widgets/radio_stats_entry.dart index eda0848a..d5fbc670 100644 --- a/lib/widgets/radio_stats_entry.dart +++ b/lib/widgets/radio_stats_entry.dart @@ -59,11 +59,15 @@ class _RadioStatsIconButtonState extends State { active: connector.radioStatsAirActivityPulse, ); if (widget.compact) { - return GestureDetector( - onTap: () => pushCompanionRadioStatsScreen(context), - child: Padding( - padding: const EdgeInsets.only(left: 4), - child: dot, + return Semantics( + label: context.l10n.radioStats_tooltip, + button: true, + child: GestureDetector( + onTap: () => pushCompanionRadioStatsScreen(context), + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: dot, + ), ), ); } diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 0973faec..5b45037d 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -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 { 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 { 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 { } }, 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 { 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 { 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), diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 2641c023..a6475cfb 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -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 { 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 { 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 { 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 { 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 { 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), diff --git a/lib/widgets/routing_sheet.dart b/lib/widgets/routing_sheet.dart new file mode 100644 index 00000000..5859e0c2 --- /dev/null +++ b/lib/widgets/routing_sheet.dart @@ -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 show(BuildContext context, {required Contact contact}) { + return showModalBottomSheet( + 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 _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 _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 _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 _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 _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 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 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 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 = [ + 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 = [ + 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( + 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, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index ca8e55ce..b42a46df 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -180,7 +180,7 @@ class _SNRIndicatorState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: Colors.grey, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/widgets/telemetry_location_map.dart b/lib/widgets/telemetry_location_map.dart new file mode 100644 index 00000000..a73fb640 --- /dev/null +++ b/lib/widgets/telemetry_location_map.dart @@ -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 createState() => _TelemetryLocationMapState(); +} + +class _TelemetryLocationMapState extends State { + 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(); + final settingsService = context.watch(); + final settings = settingsService.settings; + final tileCache = context.read(); + 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 _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( + 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, + ), + ); + } +} diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 481f1bcc..0f1561a5 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -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 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index defe6932..10f8906b 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; 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 = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* 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 */; } diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fd43b250..e082e324 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + =0.6.8 <0.7.0' + llamadart: ^0.8.0 flutter_langdetect: ^0.0.1 hooks: diff --git a/scripts/security/contact_import_clipboard_pocs.py b/scripts/security/contact_import_clipboard_pocs.py new file mode 100644 index 00000000..dcf7ced9 --- /dev/null +++ b/scripts/security/contact_import_clipboard_pocs.py @@ -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()) diff --git a/test/screens/tcp_flow_test.dart b/test/screens/tcp_flow_test.dart index 1d8174c8..d48ae33d 100644 --- a/test/screens/tcp_flow_test.dart +++ b/test/screens/tcp_flow_test.dart @@ -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); diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 16e5a951..4c6b8bd2 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -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());