From cba1e5950cb6da431aef8859b51a2e972e3e5947 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Thu, 11 Jun 2026 00:07:12 -0700 Subject: [PATCH] feat: add contact UI helpers and path editor for routing management - Implemented contactTypeIcon and contactTypeColor functions for better UI representation of contact types. - Created colorForName and firstCharacterOrEmoji functions to enhance contact display. - Developed PathEditorSheet widget for managing contact paths with a user-friendly interface. - Introduced RoutingSheet for managing contact routing modes and displaying path history. - Added a script for generating proof of concept (PoC) payloads for clipboard contact import validation. --- .claude/worktrees/agent-a01277dd8cf0dedb6 | 1 + .claude/worktrees/agent-a0c7384491c5fc807 | 1 + .claude/worktrees/agent-a1751d6addbf69e1d | 1 + .claude/worktrees/agent-a2fad933ed81b10e2 | 1 + .claude/worktrees/agent-a4d771c23db919c7f | 1 + .claude/worktrees/agent-a6675128f3ee1d6a6 | 1 + .claude/worktrees/agent-a894f10813e684f84 | 1 + .claude/worktrees/agent-a8ba0537ad42768d7 | 1 + .claude/worktrees/agent-a926d49d72757ce68 | 1 + .claude/worktrees/agent-ad2e38b6608f23ab4 | 1 + .claude/worktrees/agent-ada5836798aea71a1 | 1 + .claude/worktrees/agent-aee10bd582e9e36cf | 1 + lib/connector/meshcore_connector.dart | 266 +++-- lib/connector/meshcore_protocol.dart | 7 +- lib/connector/meshcore_uuids.dart | 4 + lib/helpers/contact_ui.dart | 59 + lib/helpers/path_helper.dart | 31 +- lib/l10n/app_en.arb | 162 +-- lib/l10n/app_localizations.dart | 534 +++++---- lib/l10n/app_localizations_bg.dart | 314 ++--- lib/l10n/app_localizations_de.dart | 313 ++--- lib/l10n/app_localizations_en.dart | 309 ++--- lib/l10n/app_localizations_es.dart | 313 ++--- lib/l10n/app_localizations_fr.dart | 315 ++--- lib/l10n/app_localizations_hu.dart | 316 ++--- lib/l10n/app_localizations_it.dart | 313 ++--- lib/l10n/app_localizations_ja.dart | 446 +++---- lib/l10n/app_localizations_ko.dart | 309 ++--- lib/l10n/app_localizations_nl.dart | 313 ++--- lib/l10n/app_localizations_pl.dart | 316 ++--- lib/l10n/app_localizations_pt.dart | 313 ++--- lib/l10n/app_localizations_ru.dart | 317 ++--- lib/l10n/app_localizations_sk.dart | 314 ++--- lib/l10n/app_localizations_sl.dart | 310 ++--- lib/l10n/app_localizations_sv.dart | 310 ++--- lib/l10n/app_localizations_uk.dart | 317 ++--- lib/l10n/app_localizations_zh.dart | 290 +++-- lib/main.dart | 29 +- lib/screens/app_debug_log_screen.dart | 21 +- lib/screens/app_settings_screen.dart | 153 +-- lib/screens/channel_chat_screen.dart | 447 ++++--- lib/screens/channel_message_path_screen.dart | 140 ++- lib/screens/channels_screen.dart | 183 +-- lib/screens/chat_screen.dart | 1031 ++++------------- lib/screens/chrome_required_screen.dart | 33 +- lib/screens/companion_radio_stats_screen.dart | 6 +- lib/screens/contacts_screen.dart | 414 ++++--- lib/screens/discovery_screen.dart | 72 +- lib/screens/line_of_sight_map_screen.dart | 40 +- lib/screens/map_cache_screen.dart | 8 +- lib/screens/map_screen.dart | 371 +++--- lib/screens/neighbors_screen.dart | 90 +- lib/screens/path_trace_map.dart | 214 ++-- lib/screens/repeater_cli_screen.dart | 121 +- lib/screens/repeater_hub_screen.dart | 28 +- lib/screens/repeater_settings_screen.dart | 125 +- lib/screens/repeater_status_screen.dart | 92 +- lib/screens/scanner_screen.dart | 178 +-- lib/screens/settings_screen.dart | 125 +- lib/screens/tcp_screen.dart | 86 +- lib/screens/telemetry_screen.dart | 97 +- lib/screens/usb_screen.dart | 145 +-- lib/services/message_retry_service.dart | 5 + lib/services/notification_service.dart | 5 +- lib/services/translation_service.dart | 31 +- lib/theme/mesh_theme.dart | 103 +- lib/widgets/byte_count_input.dart | 8 +- lib/widgets/device_tile.dart | 22 +- lib/widgets/empty_state.dart | 10 +- lib/widgets/gif_picker.dart | 10 +- lib/widgets/message_status_icon.dart | 151 ++- lib/widgets/path_editor_sheet.dart | 377 ++++++ lib/widgets/path_management_dialog.dart | 510 -------- lib/widgets/path_selection_dialog.dart | 346 ------ lib/widgets/quick_switch_bar.dart | 27 +- lib/widgets/radio_stats_entry.dart | 14 +- lib/widgets/repeater_login_dialog.dart | 19 +- lib/widgets/room_login_dialog.dart | 16 +- lib/widgets/routing_sheet.dart | 709 ++++++++++++ lib/widgets/snr_indicator.dart | 2 +- lib/widgets/telemetry_location_map.dart | 6 +- pubspec.yaml | 2 +- .../security/contact_import_clipboard_pocs.py | 71 ++ test/screens/tcp_flow_test.dart | 6 +- test/screens/usb_flow_test.dart | 18 +- untranslated.json | 988 +++++++++++++++- 86 files changed, 8149 insertions(+), 6379 deletions(-) create mode 160000 .claude/worktrees/agent-a01277dd8cf0dedb6 create mode 160000 .claude/worktrees/agent-a0c7384491c5fc807 create mode 160000 .claude/worktrees/agent-a1751d6addbf69e1d create mode 160000 .claude/worktrees/agent-a2fad933ed81b10e2 create mode 160000 .claude/worktrees/agent-a4d771c23db919c7f create mode 160000 .claude/worktrees/agent-a6675128f3ee1d6a6 create mode 160000 .claude/worktrees/agent-a894f10813e684f84 create mode 160000 .claude/worktrees/agent-a8ba0537ad42768d7 create mode 160000 .claude/worktrees/agent-a926d49d72757ce68 create mode 160000 .claude/worktrees/agent-ad2e38b6608f23ab4 create mode 160000 .claude/worktrees/agent-ada5836798aea71a1 create mode 160000 .claude/worktrees/agent-aee10bd582e9e36cf create mode 100644 lib/helpers/contact_ui.dart create mode 100644 lib/widgets/path_editor_sheet.dart delete mode 100644 lib/widgets/path_management_dialog.dart delete mode 100644 lib/widgets/path_selection_dialog.dart create mode 100644 lib/widgets/routing_sheet.dart create mode 100644 scripts/security/contact_import_clipboard_pocs.py diff --git a/.claude/worktrees/agent-a01277dd8cf0dedb6 b/.claude/worktrees/agent-a01277dd8cf0dedb6 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a01277dd8cf0dedb6 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a0c7384491c5fc807 b/.claude/worktrees/agent-a0c7384491c5fc807 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a0c7384491c5fc807 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a1751d6addbf69e1d b/.claude/worktrees/agent-a1751d6addbf69e1d new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a1751d6addbf69e1d @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a2fad933ed81b10e2 b/.claude/worktrees/agent-a2fad933ed81b10e2 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a2fad933ed81b10e2 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a4d771c23db919c7f b/.claude/worktrees/agent-a4d771c23db919c7f new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a4d771c23db919c7f @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a6675128f3ee1d6a6 b/.claude/worktrees/agent-a6675128f3ee1d6a6 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a6675128f3ee1d6a6 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a894f10813e684f84 b/.claude/worktrees/agent-a894f10813e684f84 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a894f10813e684f84 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a8ba0537ad42768d7 b/.claude/worktrees/agent-a8ba0537ad42768d7 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a8ba0537ad42768d7 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-a926d49d72757ce68 b/.claude/worktrees/agent-a926d49d72757ce68 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-a926d49d72757ce68 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-ad2e38b6608f23ab4 b/.claude/worktrees/agent-ad2e38b6608f23ab4 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-ad2e38b6608f23ab4 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-ada5836798aea71a1 b/.claude/worktrees/agent-ada5836798aea71a1 new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-ada5836798aea71a1 @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/.claude/worktrees/agent-aee10bd582e9e36cf b/.claude/worktrees/agent-aee10bd582e9e36cf new file mode 160000 index 00000000..e37616fa --- /dev/null +++ b/.claude/worktrees/agent-aee10bd582e9e36cf @@ -0,0 +1 @@ +Subproject commit e37616fa1560d6abe1286cdd5f1b329788e93ef6 diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7912962b..dee34fb0 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -160,6 +160,7 @@ class MeshCoreConnector extends ChangeNotifier { {}; // contactPubKeyHex -> Set of "targetHash_emoji" StreamSubscription>? _scanSubscription; + StreamSubscription? _isScanningSubscription; StreamSubscription? _connectionSubscription; StreamSubscription>? _notifySubscription; Timer? _notifyListenersTimer; @@ -507,10 +508,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); @@ -1356,6 +1369,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); @@ -1409,20 +1437,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 { @@ -1430,15 +1478,13 @@ class MeshCoreConnector extends ChangeNotifier { final systemDevices = await FlutterBluePlus.systemDevices([ Guid(MeshCoreUuids.service), ]); + // systemDevices is already filtered by the NUS service UUID above, so no + // additional name-prefix filtering is applied here. This keeps Linux + // discovery name-agnostic and consistent with the main scan path. _linuxSystemScanResults ..clear() ..addAll( systemDevices - .where( - (device) => MeshCoreUuids.deviceNamePrefixes.any( - device.platformName.startsWith, - ), - ) .map( (device) => ScanResult( device: device, @@ -1499,9 +1545,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); } } @@ -1743,6 +1797,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, @@ -1911,18 +1976,71 @@ class MeshCoreConnector extends ChangeNotifier { } } } else { - try { - await device.connect( + Future attemptConnect() { + return device.connect( timeout: connectTimeout, mtu: null, license: License.free, ); + } + + // 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; + } } } @@ -3237,6 +3355,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); @@ -3450,12 +3569,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 { @@ -3929,8 +4046,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; @@ -4275,7 +4392,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); @@ -5521,6 +5645,9 @@ class MeshCoreConnector extends ChangeNotifier { } messages.add(message); + if (messages.length > _messageWindowSize) { + messages.removeRange(0, messages.length - _messageWindowSize); + } _messageStore.saveMessages(pubKeyHex, messages); notifyListeners(); } @@ -5907,20 +6034,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; } @@ -6152,6 +6283,7 @@ class MeshCoreConnector extends ChangeNotifier { @override void dispose() { _scanSubscription?.cancel(); + _isScanningSubscription?.cancel(); _connectionSubscription?.cancel(); _usbFrameSubscription?.cancel(); _notifySubscription?.cancel(); @@ -6210,82 +6342,6 @@ class MeshCoreConnector extends ChangeNotifier { } } - void importContact(Uint8List frame) { - final packet = BufferReader(frame); - int payloadType = 0; - Uint8List pathBytes = Uint8List(0); - try { - packet.skipBytes(1); // Skip frame type byte - packet.skipBytes(1); // Skip SNR byte - packet.skipBytes(1); // Skip RSSI byte - final header = packet.readByte(); - final routeType = header & 0x03; - payloadType = (header >> 2) & 0x0F; - if (routeType == _routeTransportFlood || - routeType == _routeTransportDirect) { - packet.skipBytes(4); // Skip transport-specific bytes - } - //final payloadVer = (header >> 6) & 0x03; - final pathLenRaw = packet.readByte(); - final pathByteLen = _decodePathByteLen(pathLenRaw); - pathBytes = packet.readBytes(pathByteLen); - } catch (e) { - appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); - return; - } - double? latitude; - double? longitude; - String name = ''; - Uint8List publicKey = Uint8List(0); - int type = 0; - int timestamp = 0; - bool hasLocation = false; - bool hasName = false; - if (payloadType != payloadTypeADVERT) { - appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector'); - return; - } - try { - publicKey = packet.readBytes(32); - timestamp = packet.readInt32LE(); - //TODO add signature verification - packet.skipBytes(64); // Skip signature for now - final flags = packet.readByte(); - type = flags & 0x0F; - hasLocation = (flags & 0x10) != 0; - // For future use: - //final hasFeature1 = (flags & 0x20) != 0; - //final hasFeature2 = (flags & 0x40) != 0; - hasName = (flags & 0x80) != 0; - if (hasLocation && packet.remaining >= 8) { - latitude = packet.readInt32LE() / 1e6; - longitude = packet.readInt32LE() / 1e6; - } - if (hasName && packet.remaining > 0) { - name = packet.readCString(); - } - } catch (e) { - appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); - return; - } - - importDiscoveredContact( - Contact( - rawPacket: frame, - publicKey: publicKey, - name: name, - type: type, - pathLength: pathBytes.isEmpty ? -1 : pathBytes.length, - path: Uint8List.fromList( - pathBytes.reversed.toList(), - ), // Store path in reverse for easier use in outgoing messages - latitude: latitude, - longitude: longitude, - lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), - ), - ); - } - bool hasValidLocation(double? latitude, double? longitude) { const double epsilon = 1e-6; final lat = latitude ?? 0.0; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index a056f4ac..7213b753 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -457,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; 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/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 fe51d636..bd599c00 100644 --- a/lib/helpers/path_helper.dart +++ b/lib/helpers/path_helper.dart @@ -8,24 +8,29 @@ class PathHelper { .join(','); } + static String hopHex(int byte) { + return byte.toRadixString(16).padLeft(2, '0').toUpperCase(); + } + + static String? hopName(int byte, List allContacts) { + final matches = allContacts + .where( + (c) => + 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 String resolvePathNames( List pathBytes, List allContacts, ) { return pathBytes - .map((b) { - final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase(); - final matches = allContacts - .where( - (c) => - c.publicKey.first == b && - (c.type == advTypeRepeater || c.type == advTypeRoom), - ) - .toList(); - if (matches.isEmpty) return hex; - if (matches.length == 1) return matches.first.name; - return matches.map((c) => c.name).join(' | '); - }) + .map((b) => hopName(b, allContacts) ?? hopHex(b)) .join(' \u2192 '); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1f05124a..58613d02 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": "—", @@ -297,17 +303,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})", @@ -453,6 +448,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", @@ -777,15 +775,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": { @@ -794,31 +783,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.", @@ -1100,41 +1138,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", @@ -1161,9 +1166,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}", @@ -2485,5 +2487,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_localizations.dart b/lib/l10n/app_localizations.dart index b45facfe..f55fc02c 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: @@ -1534,12 +1570,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: @@ -1882,6 +1912,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: @@ -2710,78 +2758,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: @@ -2794,71 +2776,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. /// @@ -3687,90 +3849,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: @@ -3891,24 +3981,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: @@ -7587,6 +7659,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 af9e693d..68742652 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 => '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 => 'Рестартирай'; @@ -796,11 +814,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 => 'Батерия'; @@ -1002,6 +1015,15 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_newGroup => 'Нова група'; + @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 => 'Група'; @@ -1482,35 +1504,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( @@ -1522,12 +1515,6 @@ class AppLocalizationsBg extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Успехи'; - - @override - String get chat_score => 'Score'; - @override String get chat_removePath => 'Премахни пътя'; @@ -1535,52 +1522,144 @@ 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 => 'Routing'; @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 => '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 => 'Запазено локално. Свържете се за синхронизиране.'; @@ -2050,66 +2129,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 => 'Управление на повторители'; @@ -2176,16 +2202,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 => 'Презареди'; @@ -4437,4 +4453,28 @@ class AppLocalizationsBg 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_de.dart b/lib/l10n/app_localizations_de.dart index 3fa9c4a8..62df5bb6 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 => '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 => 'Neustart'; @@ -792,11 +810,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'; @@ -998,6 +1011,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_newGroup => 'Neue Gruppe'; + @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 => 'Gruppenname'; @@ -1480,36 +1502,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( @@ -1521,12 +1513,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'; @@ -1534,51 +1520,144 @@ 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 => 'Routing'; @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 => '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 => 'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.'; @@ -2049,65 +2128,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'; @@ -2172,16 +2199,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'; @@ -4455,4 +4472,28 @@ class AppLocalizationsDe 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_en.dart b/lib/l10n/app_localizations_en.dart index 4fbfd5bb..eecfbc41 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'; @@ -777,11 +795,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'; @@ -981,6 +994,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'; @@ -1452,34 +1474,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( @@ -1491,12 +1485,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'; @@ -1504,50 +1492,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.'; @@ -2009,64 +2091,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'; @@ -2130,15 +2160,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'; @@ -4360,4 +4381,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 383dd032..aa17930b 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 => '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 => 'Reiniciar'; @@ -791,11 +809,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'; @@ -997,6 +1010,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_newGroup => 'Nuevo Grupo'; + @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 => 'Nombre del grupo'; @@ -1479,34 +1501,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( @@ -1518,12 +1512,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'; @@ -1531,53 +1519,144 @@ 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 => 'Routing'; @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 => '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 => 'Guardado localmente. Conéctate para sincronizar.'; @@ -2045,66 +2124,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'; @@ -2169,15 +2195,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'; @@ -4442,4 +4459,28 @@ class AppLocalizationsEs 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_fr.dart b/lib/l10n/app_localizations_fr.dart index 990c49c6..69e2af85 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 => '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 => 'Redémarrer'; @@ -797,11 +815,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'; @@ -1003,6 +1016,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_newGroup => 'Nouveau Groupe'; + @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 => 'Nom du groupe'; @@ -1485,35 +1507,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( @@ -1525,12 +1518,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'; @@ -1538,53 +1525,144 @@ 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 => 'Routing'; @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 => '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 => 'Sauvegardé localement. Connectez-vous pour synchroniser.'; @@ -2056,66 +2134,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'; @@ -2181,16 +2206,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'; @@ -4471,4 +4486,28 @@ class AppLocalizationsFr 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_hu.dart b/lib/l10n/app_localizations_hu.dart index 58f263b3..f58b8afc 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 => '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 => 'Újraindítás'; @@ -795,11 +813,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'; @@ -1003,6 +1016,15 @@ class AppLocalizationsHu extends AppLocalizations { @override String get contacts_newGroup => 'Új csoport'; + @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 => 'Csoport neve'; @@ -1488,36 +1510,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( @@ -1529,12 +1521,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'; @@ -1542,52 +1528,144 @@ 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 => '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 => 'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.'; @@ -2058,67 +2136,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'; @@ -2184,16 +2208,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'; @@ -4458,4 +4472,28 @@ class AppLocalizationsHu 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_it.dart b/lib/l10n/app_localizations_it.dart index a0149bd0..7a7043bb 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 => '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 => 'Riavvia'; @@ -794,11 +812,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'; @@ -999,6 +1012,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_newGroup => 'Nuovo Gruppo'; + @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 => 'Nome gruppo'; @@ -1481,34 +1503,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( @@ -1520,12 +1514,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'; @@ -1533,53 +1521,144 @@ 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 => 'Routing'; @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 => '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 => 'Salvatato localmente. Connetti per sincronizzare.'; @@ -2047,66 +2126,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'; @@ -2173,15 +2199,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'; @@ -4447,4 +4464,28 @@ class AppLocalizationsIt 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_ja.dart b/lib/l10n/app_localizations_ja.dart index 12396086..ee79c6ee 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 => '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 => '再起動'; @@ -559,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 => @@ -709,7 +727,7 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_autoRouteRotation => '自動ルートの切り替え'; @override - String get appSettings_autoRouteRotationSubtitle => '最適なルートと、洪水モードを切り替える'; + String get appSettings_autoRouteRotationSubtitle => '最適なルートと、フラッドモードを切り替える'; @override String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています'; @@ -751,11 +769,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 => 'バッテリー'; @@ -786,7 +799,7 @@ class AppLocalizationsJa extends AppLocalizations { String get appSettings_showRepeaters => '繰り返し再生機能'; @override - String get appSettings_showRepeatersSubtitle => '地図上にリピーターノードを表示する'; + String get appSettings_showRepeatersSubtitle => '地図上にリピータノードを表示する'; @override String get appSettings_showChatNodes => 'チャットノードの表示'; @@ -925,7 +938,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get contacts_manageRepeater => 'リピーターの管理'; + String get contacts_manageRepeater => 'リピータの管理'; @override String get contacts_manageRoom => 'ルームサーバーの管理'; @@ -950,6 +963,15 @@ class AppLocalizationsJa extends AppLocalizations { @override String get contacts_newGroup => '新しいグループ'; + @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 => 'グループ名'; @@ -1416,33 +1438,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( @@ -1454,61 +1449,149 @@ 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 => '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 => 'ローカルで保存。同期のために接続する。'; @@ -1531,7 +1614,7 @@ class AppLocalizationsJa extends AppLocalizations { String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する'; @override - String get chat_floodForced => '洪水(強制的な)'; + String get chat_floodForced => 'フラッド(強制的な)'; @override String get chat_directForced => '直接的な(強制的な)'; @@ -1542,7 +1625,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get chat_floodAuto => '洪水 (自動)'; + String get chat_floodAuto => 'フラッド (自動)'; @override String get chat_direct => '直接'; @@ -1607,7 +1690,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_chat => 'チャット'; @override - String get map_repeater => '繰り返し送信装置'; + String get map_repeater => 'リピータ'; @override String get map_room => '部屋'; @@ -1702,13 +1785,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 => '主要なプレフィックス'; @@ -1747,7 +1830,7 @@ class AppLocalizationsJa extends AppLocalizations { String get map_joinRoom => '部屋に参加する'; @override - String get map_manageRepeater => 'リピーターの管理'; + String get map_manageRepeater => 'リピータの管理'; @override String get map_tapToAdd => 'ノードをクリックして、パスに追加します。'; @@ -1920,7 +2003,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get login_repeaterDescription => - '設定やステータスにアクセスするために、リピーターのパスワードを入力してください。'; + '設定やステータスにアクセスするために、リピータのパスワードを入力してください。'; @override String get login_roomDescription => '設定やステータスへのアクセスには、部屋のパスワードを入力してください。'; @@ -1935,7 +2018,7 @@ class AppLocalizationsJa extends AppLocalizations { String get login_autoUseSavedPath => '自動 (保存されたパスを使用)'; @override - String get login_forceFloodMode => '強制的に洪水モードを起動'; + String get login_forceFloodMode => '強制的にフラッドモードを起動'; @override String get login_managePaths => 'パスの管理'; @@ -1963,70 +2046,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 => 'ルームサーバーに関する情報'; @@ -2041,7 +2074,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_status => 'ステータス'; @override - String get repeater_statusSubtitle => 'リピーターの状態、統計情報、および隣接するネットワークの情報を表示する'; + String get repeater_statusSubtitle => 'リピータの状態、統計情報、および隣接するネットワークの情報を表示する'; @override String get repeater_telemetry => 'テレメトリー'; @@ -2053,7 +2086,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cli => 'CLI(コマンドラインインターフェース)'; @override - String get repeater_cliSubtitle => 'リピーターへのコマンドを送信する'; + String get repeater_cliSubtitle => 'リピータへのコマンドを送信する'; @override String get repeater_neighbors => '近隣住民'; @@ -2065,7 +2098,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_settings => '設定'; @override - String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する'; + String get repeater_settingsSubtitle => 'リピータのパラメータを設定する'; @override String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する'; @@ -2080,15 +2113,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 => 'リフレッシュ'; @@ -2163,12 +2187,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 @@ -2182,16 +2206,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 => '管理者パスワード'; @@ -2251,7 +2275,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_packetForwarding => 'パケット転送'; @override - String get repeater_packetForwardingSubtitle => 'リピーターがパケットを転送できるように設定する'; + String get repeater_packetForwardingSubtitle => 'リピータがパケットを転送できるように設定する'; @override String get repeater_guestAccess => 'ゲストへのアクセス'; @@ -2277,7 +2301,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => '洪水に関する広告の表示間隔'; + String get repeater_floodAdvertInterval => 'フラッドに関する広告の表示間隔'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2291,13 +2315,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キーの再生成'; @@ -2307,17 +2331,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機能は、シリアルコンソール経由でのみ利用可能です。'; @@ -2363,7 +2387,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_loopDetect => 'ループ検出'; @override - String get repeater_loopDetectHelper => 'ルーティングループに見えるような、洪水パケットを送信する'; + String get repeater_loopDetectHelper => 'ルーティングループを検知する'; @override String get repeater_loopDetectOff => 'オフ'; @@ -2392,7 +2416,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_ownerInfo => '事業者の情報'; @override - String get repeater_ownerInfoHelper => 'このリピーターに関する公開メタデータ'; + String get repeater_ownerInfoHelper => 'このリピータに関する公開メタデータ'; @override String get repeater_refreshOwnerInfo => 'オペレーター情報の更新'; @@ -2401,7 +2425,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 => '高度な'; @@ -2414,14 +2438,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_pathHashModeHelper => - 'このリピーターのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。'; + 'このリピータの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遅延'; @@ -2448,16 +2472,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 => '現在、時刻を同期する'; @@ -2477,7 +2501,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_settingsSavedRebootNeeded => - '設定を保存しました — リピーターを再起動して適用してください'; + '設定を保存しました — リピータを再起動して適用してください'; @override String repeater_settingsPartialFailure(String failures) { @@ -2518,7 +2542,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get repeater_cliTitle => 'リピーターのコマンドラインインターフェース'; + String get repeater_cliTitle => 'リピータのコマンドラインインターフェース'; @override String get repeater_debugNextCommand => '次のコマンドのデバッグ'; @@ -2609,7 +2633,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 => @@ -2617,7 +2641,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpSetFloodMax => - 'インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。'; + 'インバウンドフラッドパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。'; @override String get repeater_cliHelpSetIntThresh => @@ -2636,7 +2660,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpSetFloodAdvertInterval => - '洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。'; + 'フラッド広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。'; @override String get repeater_cliHelpSetGuestPassword => @@ -2710,7 +2734,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'ゼロホップ広告を通じて受信した他のリピーターノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。'; + 'ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。'; @override String get repeater_cliHelpNeighborRemove => @@ -2718,7 +2742,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpRegion => - '(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。'; + '(特定のシリーズのみ)定義されたすべての地域と、現在のフラッド許可状況を一覧表示します。'; @override String get repeater_cliHelpRegionLoad => @@ -2737,7 +2761,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpRegionAllowf => - '指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)'; + '指定された領域に対して、「フラッド」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)'; @override String get repeater_cliHelpRegionDenyf => @@ -2793,7 +2817,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_logging => 'ログ記録'; @override - String get repeater_neighborsRepeaterOnly => '近隣住民(リピーターのみ)'; + String get repeater_neighborsRepeaterOnly => '近隣住民(リピータのみ)'; @override String get repeater_regionManagementRepeaterOnly => '地域管理(ブロードキャスト用のみ)'; @@ -2838,7 +2862,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpDiscoverNeighbors => - '近隣のノードに対して、ノードの探索リクエストを送信します。(中継機能のみ)'; + '近隣のノードに対して、ノードの探索リクエストを送信します。(リピータ機能のみ)'; @override String get repeater_cliHelpPowersaving => '省電力モードがオンになっているかどうかを表示します。'; @@ -2887,7 +2911,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetName => '設定されたノードの名前を表示します。'; @override - String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピーター、ルームサーバーなど)を表示します。'; + String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。'; @override String get repeater_cliHelpGetPublicKey => 'デバイスの公開鍵を表示します。'; @@ -2897,7 +2921,7 @@ class AppLocalizationsJa extends AppLocalizations { '(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。'; @override - String get repeater_cliHelpGetRepeat => 'パケット転送(リピーター機能)が有効になっているかどうかを表示します。'; + String get repeater_cliHelpGetRepeat => 'パケット転送(リピータ機能)が有効になっているかどうかを表示します。'; @override String get repeater_cliHelpGetTx => '現在のTX(送信)電力のdBm値を表示します。'; @@ -2936,7 +2960,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliHelpGetFloodAdvertInterval => - '洪水に関する広告の放送時間を時間単位で表示します。'; + 'フラッドに関する広告の放送時間を時間単位で表示します。'; @override String get repeater_cliHelpGetGuestPassword => '設定されたゲストパスワードを表示します。'; @@ -2951,13 +2975,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 => '所有者の連絡先情報を表示します。'; @@ -2969,7 +2993,7 @@ class AppLocalizationsJa extends AppLocalizations { String get repeater_cliHelpGetLoopDetect => 'ループ検出の感度を示す。'; @override - String get repeater_cliHelpGetAcl => '(シリアルのみ)リピーター上のアクセス制御設定を一覧表示します。'; + String get repeater_cliHelpGetAcl => '(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。'; @override String get repeater_cliHelpGetBridgeEnabled => '橋が有効になっているかどうかを表示します。'; @@ -3032,10 +3056,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 => '(シリアルのみ)パケットレベルの統計情報を表示します。'; @@ -3245,7 +3269,7 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get neighbors_repeatersNeighbors => '繰り返し送信する、近隣'; + String get neighbors_repeatersNeighbors => '近隣のリピータ'; @override String get neighbors_noData => '近隣のデータは利用できません。'; @@ -3270,7 +3294,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_otherObservedPaths => '観察されたその他の経路'; @override - String get channelPath_repeaterHops => 'ホップの繰り返し'; + String get channelPath_repeaterHops => 'リピータホップ'; @override String get channelPath_noHopDetails => 'このパッケージに関する詳細な情報は提供されていません。'; @@ -3285,7 +3309,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_timeLabel => '時間'; @override - String get channelPath_repeatsLabel => '繰り返し'; + String get channelPath_repeatsLabel => 'リピータ'; @override String channelPath_pathLabel(int index) { @@ -3317,7 +3341,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_unknownPath => '不明'; @override - String get channelPath_floodPath => '洪水'; + String get channelPath_floodPath => 'フラッド'; @override String get channelPath_directPath => '直接'; @@ -3336,7 +3360,7 @@ class AppLocalizationsJa extends AppLocalizations { String get channelPath_mapTitle => '経路図'; @override - String get channelPath_noRepeaterLocations => 'この経路には、中継装置の設置場所がありません。'; + String get channelPath_noRepeaterLocations => 'この経路にリピータの位置情報はありません。'; @override String channelPath_primaryPath(int index) { @@ -3556,7 +3580,7 @@ class AppLocalizationsJa extends AppLocalizations { String get listFilter_users => '利用者'; @override - String get listFilter_repeaters => '繰り返し送信装置'; + String get listFilter_repeaters => 'リピータ'; @override String get listFilter_roomServers => 'ルーム用サーバー'; @@ -3756,10 +3780,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 => '部屋のサーバーへの経路を追跡する'; @@ -3791,7 +3815,7 @@ class AppLocalizationsJa extends AppLocalizations { String get contacts_zeroHopAdvert => 'ゼロホップ広告'; @override - String get contacts_floodAdvert => '洪水に関する広告'; + String get contacts_floodAdvert => 'フラッドに関する広告'; @override String get contacts_copyAdvertToClipboard => '広告をクリップボードにコピー'; @@ -3862,7 +3886,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 => @@ -3895,7 +3919,7 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_gpxExportError => 'エクスポート時にエラーが発生しました。'; @override - String get settings_gpxExportRepeatersRoom => '中継装置およびルームサーバーの設置場所'; + String get settings_gpxExportRepeatersRoom => 'リピータ/ルームサーバーの位置情報'; @override String get settings_gpxExportChat => '関連施設'; @@ -3911,7 +3935,7 @@ class AppLocalizationsJa extends AppLocalizations { 'meshcore-open GPX形式の地図データのエクスポート'; @override - String get snrIndicator_nearByRepeaters => '近くの電波中継局'; + String get snrIndicator_nearByRepeaters => '近くのリピータ'; @override String get snrIndicator_lastSeen => '最後に確認された場所'; @@ -3933,11 +3957,11 @@ class AppLocalizationsJa extends AppLocalizations { '利用者が自動的に発見したユーザーを追加できるようにする。'; @override - String get contactsSettings_autoAddRepeatersTitle => '自動で繰り返し設定'; + String get contactsSettings_autoAddRepeatersTitle => 'リピータを自動追加'; @override String get contactsSettings_autoAddRepeatersSubtitle => - '発見した中継局を、自動的に追加できるようにする。'; + '発見したリピータを、自動的に追加できるようにする。'; @override String get contactsSettings_autoAddRoomServersTitle => '自動でルームサーバーを追加'; @@ -4212,4 +4236,28 @@ class AppLocalizationsJa 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_ko.dart b/lib/l10n/app_localizations_ko.dart index 606a33dc..64c5a6a6 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 => '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 => '재부팅'; @@ -749,11 +767,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 => '배터리'; @@ -945,6 +958,15 @@ class AppLocalizationsKo extends AppLocalizations { @override String get contacts_newGroup => '새로운 그룹'; + @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 => '그룹 이름'; @@ -1411,34 +1433,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( @@ -1450,61 +1444,149 @@ 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 => '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 => '로컬에 저장. 동기화 연결'; @@ -1958,64 +2040,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 => '리피터 관리'; @@ -2077,15 +2107,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 => '새롭게'; @@ -4213,4 +4234,28 @@ class AppLocalizationsKo 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_nl.dart b/lib/l10n/app_localizations_nl.dart index 90a446df..6f4d9fbb 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 => '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 => 'Herstarten'; @@ -786,11 +804,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'; @@ -991,6 +1004,15 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_newGroup => 'Nieuwe Groep'; + @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 => 'Groepnaam'; @@ -1468,34 +1490,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( @@ -1507,12 +1501,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'; @@ -1520,52 +1508,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 => 'Routing'; @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 => '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 => 'Opgeslagen lokaal. Verbinden om te synchroniseren.'; @@ -2034,65 +2114,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'; @@ -2157,16 +2184,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'; @@ -4422,4 +4439,28 @@ class AppLocalizationsNl 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_pl.dart b/lib/l10n/app_localizations_pl.dart index f30cbb7f..f582d2df 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 => '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 => 'Uruchom ponownie'; @@ -796,11 +814,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'; @@ -1009,6 +1022,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_newGroup => 'Nowa Grupa'; + @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 => 'Nazwa grupy'; @@ -1491,35 +1513,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( @@ -1533,12 +1526,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ę'; @@ -1546,52 +1533,144 @@ 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 => 'Routing'; @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 => '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 => 'Zapisano lokalnie. Połącz się, aby zsynchronizować.'; @@ -2061,68 +2140,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'; @@ -2187,16 +2211,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ż'; @@ -4459,4 +4473,28 @@ class AppLocalizationsPl 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_pt.dart b/lib/l10n/app_localizations_pt.dart index 5fe15028..23495d55 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 => '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 => 'Reiniciar'; @@ -793,11 +811,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'; @@ -999,6 +1012,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_newGroup => 'Novo Grupo'; + @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 => 'Nome do grupo'; @@ -1478,34 +1500,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( @@ -1517,12 +1511,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'; @@ -1530,53 +1518,144 @@ 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 => 'Routing'; @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 => '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 => 'Salvo localmente. Conectar para sincronizar.'; @@ -2044,66 +2123,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'; @@ -2168,15 +2194,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'; @@ -4435,4 +4452,28 @@ class AppLocalizationsPt 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_ru.dart b/lib/l10n/app_localizations_ru.dart index 84929424..fe535172 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 => '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 => 'Перезагрузить'; @@ -795,11 +813,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 => 'Батарея'; @@ -1000,6 +1013,15 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contacts_newGroup => 'Новая группа'; + @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 => 'Имя группы'; @@ -1479,35 +1501,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( @@ -1521,12 +1514,6 @@ class AppLocalizationsRu extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'успешно'; - - @override - String get chat_score => 'Оценка'; - @override String get chat_removePath => 'Удалить маршрут'; @@ -1534,54 +1521,144 @@ 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 => 'Routing'; @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 => '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 => 'Сохранено локально. Подключитесь для синхронизации.'; @@ -2049,66 +2126,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 => 'Управление репитером'; @@ -2173,16 +2196,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 => 'Обновить'; @@ -4453,4 +4466,28 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contact_typeUnknown => 'Неизвестно'; + + @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_sk.dart b/lib/l10n/app_localizations_sk.dart index 2072b634..f15bcfc1 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 => '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 => 'Restartovať'; @@ -782,11 +800,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'; @@ -988,6 +1001,15 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_newGroup => 'Nová skupina'; + @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 => 'Názov skupiny'; @@ -1469,35 +1491,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( @@ -1509,12 +1502,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'; @@ -1522,52 +1509,144 @@ 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 => 'Routing'; @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 => '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 => 'Uložené lokálne. Spojte sa na synchronizáciu.'; @@ -2035,66 +2114,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'; @@ -2159,16 +2185,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ť'; @@ -4418,4 +4434,28 @@ class AppLocalizationsSk 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_sl.dart b/lib/l10n/app_localizations_sl.dart index e91086e9..c2dc192b 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -92,6 +92,24 @@ class AppLocalizationsSl extends AppLocalizations { @override String get common_disable => 'Izklopiti'; + @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 => 'Ponoviti'; @@ -783,11 +801,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'; @@ -987,6 +1000,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_newGroup => 'Nova skupina'; + @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 => 'Ime skupine'; @@ -1466,34 +1488,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( @@ -1505,12 +1499,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'; @@ -1518,51 +1506,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 => 'Routing'; @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 => '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 => 'Shrano lokalno. Povežite se za sinhronizacijo.'; @@ -2032,65 +2113,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'; @@ -2156,15 +2185,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'; @@ -4416,4 +4436,28 @@ class AppLocalizationsSl 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_sv.dart b/lib/l10n/app_localizations_sv.dart index 95af0f64..ef0df94c 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 => '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 => 'Start om'; @@ -777,11 +795,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'; @@ -982,6 +995,15 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_newGroup => 'Ny grupp'; + @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 => 'Gruppnamn'; @@ -1460,35 +1482,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( @@ -1500,12 +1493,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'; @@ -1513,50 +1500,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 => 'Routing'; @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 => '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 => 'Sparat lokalt. Anslut för att synkronisera.'; @@ -2021,65 +2102,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'; @@ -2144,15 +2173,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'; @@ -4391,4 +4411,28 @@ class AppLocalizationsSv 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_uk.dart b/lib/l10n/app_localizations_uk.dart index ae82d64e..beb14024 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 => '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 => 'Перезавантажити'; @@ -789,11 +807,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 => 'Батарея'; @@ -995,6 +1008,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_newGroup => 'Нова група'; + @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 => 'Назва групи'; @@ -1474,35 +1496,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( @@ -1516,12 +1509,6 @@ class AppLocalizationsUk extends AppLocalizations { return '$count $_temp0'; } - @override - String get chat_successes => 'Успішно'; - - @override - String get chat_score => 'Оцінка'; - @override String get chat_removePath => 'Видалити шлях'; @@ -1529,54 +1516,144 @@ 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 => 'Routing'; @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 => '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 => 'Збережено локально. Підключіться для синхронізації.'; @@ -2043,67 +2120,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 => 'Керування ретранслятором'; @@ -2168,16 +2191,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 => 'Оновити'; @@ -4454,4 +4467,28 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contact_typeUnknown => 'Невідомо'; + + @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_zh.dart b/lib/l10n/app_localizations_zh.dart index 79e4e22e..41374057 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 => '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 => '重启'; @@ -735,11 +753,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 => '电池'; @@ -931,6 +944,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_newGroup => '新建群聊'; + @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 => '群聊名称'; @@ -1397,85 +1419,154 @@ 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 => 'Routing'; @override - String chat_pathSetHops(int hopCount, String status) { - return '路径设置:$hopCount 跳 - $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 => '已本地保存,连接设备后可同步。'; @@ -1928,54 +2019,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 => '转发节点管理'; @@ -2036,15 +2085,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 => '刷新'; @@ -4086,4 +4126,28 @@ class AppLocalizationsZh 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/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..719e5735 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,7 @@ class AppDebugLogScreen extends StatelessWidget { entry.formattedTime, style: TextStyle( fontSize: 10, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ); @@ -88,14 +88,14 @@ 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 +103,7 @@ class AppDebugLogScreen extends StatelessWidget { context.l10n.debugLog_enableInSettings, style: TextStyle( fontSize: 12, - color: Colors.grey[500], + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], @@ -115,18 +115,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..76ce1f07 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,10 +349,16 @@ class AppSettingsScreen extends StatelessWidget { ); }, ), - if (settingsService.settings.autoRouteRotationEnabled) ...[ - const Divider(height: 1), - ListTile( - title: Text(context.l10n.appSettings_maxRouteWeight), + if (settingsService.settings.autoRouteRotationEnabled) + Container( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + padding: const EdgeInsets.only(left: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxRouteWeight), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -454,7 +466,21 @@ class AppSettingsScreen extends StatelessWidget { ], ), ), - ], + ], + ), + ), + const Divider(height: 1), + SwitchListTile( + secondary: const Icon(Icons.location_searching), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, + ), + value: settingsService.settings.enableMessageTracing, + onChanged: (value) { + settingsService.setEnableMessageTracing(value); + }, + ), ], ), ); @@ -584,15 +610,15 @@ 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 +629,15 @@ 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 +897,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 b5e151bd..c43c6c67 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'; @@ -25,8 +25,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'; @@ -283,6 +284,8 @@ class _ChannelChatScreenState extends State { ) : widget.channel.name, style: const TextStyle(fontSize: 16), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), Consumer( builder: (context, connector, _) { @@ -311,9 +314,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) => [ @@ -321,11 +322,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, + ), ), ], ), @@ -344,34 +351,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, ), ); } @@ -381,6 +371,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) { @@ -416,14 +425,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 @@ -495,7 +510,8 @@ class _ChannelChatScreenState extends State { const maxSwipeOffset = 64.0; const replySwipeThreshold = 64.0; const bodyFontSize = 14.0; - final messageBody = Column( + final messageBody = LayoutBuilder( + builder: (context, constraints) => Column( crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end : CrossAxisAlignment.start, @@ -512,9 +528,6 @@ class _ChannelChatScreenState extends State { ], Flexible( child: GestureDetector( - onTap: PlatformInfo.isDesktop - ? null - : () => _showMessagePathInfo(message), onLongPress: () => _showMessageActions(message), onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => _showMessageActions(message) @@ -524,7 +537,7 @@ class _ChannelChatScreenState extends State { ? const EdgeInsets.all(4) : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, + maxWidth: constraints.maxWidth * 0.65, ), decoration: BoxDecoration( color: isOutgoing @@ -566,20 +579,6 @@ class _ChannelChatScreenState extends State { 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( @@ -599,36 +598,6 @@ class _ChannelChatScreenState extends State { .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 @@ -651,97 +620,89 @@ class _ChannelChatScreenState extends State { ), ), ), - if (!enableTracing && isOutgoing) ...[ - const SizedBox(width: 4), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: MessageStatusIcon( - isAcked: - message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty, - isFailed: - message.status == - ChannelMessageStatus.failed, + ], + ), + if (enableTracing && displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.channels_via( + _formatPathPrefixes(displayPath), + ), + style: TextStyle( + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatTime(context, message.timestamp), + style: TextStyle( + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + if (enableTracing && message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11 * textScale, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, ), ), ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent, + isRepeated: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isPending: + message.status == + ChannelMessageStatus.pending, + isFailed: + message.status == + ChannelMessageStatus.failed, + ), + ], ], ), - if (enableTracing) ...[ - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.channels_via( - _formatPathPrefixes(displayPath), - ), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatTime(context, message.timestamp), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), - Text( - '${message.repeatCount}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ], - 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: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], - ), - ), - ], + ), ], ), ), @@ -757,6 +718,7 @@ class _ChannelChatScreenState extends State { ), ], ], + ), ); if (!isOutgoing && !PlatformInfo.isDesktop) { @@ -958,9 +920,9 @@ 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}', @@ -1020,8 +982,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, @@ -1037,36 +999,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( @@ -1116,8 +1048,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), ), ], ), @@ -1412,15 +1343,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( @@ -1463,19 +1393,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), - ), ], ), ), @@ -1516,6 +1448,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; @@ -1557,6 +1517,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { double _maxSwipeDistance = 0; int? _swipePointerId; bool _swipeLockedToHorizontal = false; + bool _isRtl = false; void _handleSwipeStart(Offset position) { _swipeStartPosition = position; @@ -1577,11 +1538,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; @@ -1593,28 +1556,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(); @@ -1654,6 +1621,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, @@ -1667,7 +1638,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 afb2b906..6e9f3882 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -106,7 +106,7 @@ 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), @@ -131,22 +131,25 @@ 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(message.pathLength, l10n), ), if (observedLabel != null) - _buildDetailRow(l10n.channelPath_observedLabel, observedLabel), + _buildDetailRow(context, l10n.channelPath_observedLabel, observedLabel), ], ), ), @@ -250,7 +253,7 @@ class ChannelMessagePathScreen extends StatelessWidget { return l10n.channelPath_observedSomeOf(observedCount, pathLength); } - Widget _buildDetailRow(String label, String value) { + Widget _buildDetailRow(BuildContext context, String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( @@ -258,7 +261,7 @@ 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)), ], @@ -412,17 +415,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, @@ -593,7 +596,7 @@ 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( @@ -664,7 +667,7 @@ class _ChannelMessagePathMapScreenState label, _formatPathPrefixes(selectedPath.pathBytes), ), - style: TextStyle(color: Colors.grey[700], fontSize: 12), + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12), ), ], ), @@ -685,28 +688,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, ), ), ), @@ -729,28 +736,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, ), ), ), @@ -804,6 +815,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, @@ -826,9 +843,22 @@ 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 40726021..b725cc61 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,22 @@ 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( + ? 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, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - ], ), ), - ), - ], + ], + ), ) : (viewState.channelsSortOption == ChannelSortOption.manual && @@ -357,6 +349,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; @@ -449,37 +444,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, @@ -540,10 +543,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); @@ -725,23 +733,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, @@ -929,7 +953,7 @@ 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, @@ -1043,7 +1067,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, ), ), @@ -1214,6 +1240,7 @@ class _ChannelsScreenState extends State child: FilledButton( onPressed: () async { final name = nameController.text.trim(); + final publicLabel = context.l10n.channels_public; if (name.isEmpty) { showDismissibleSnackBar( context, @@ -1238,7 +1265,7 @@ class _ChannelsScreenState extends State final psk = community .deriveCommunityPublicPsk(); final channelName = - '${community.name} Public'; + '${community.name} $publicLabel'; connector.setChannel( nextIndex, channelName, @@ -1594,7 +1621,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), ), ), ], @@ -1604,7 +1631,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), @@ -1653,14 +1680,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), @@ -1668,7 +1700,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, ), @@ -1692,10 +1727,12 @@ 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( @@ -1722,14 +1759,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, + ), ), ], ), @@ -1830,7 +1873,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 5e9f960e..b96095bf 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,26 @@ 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 +213,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 +285,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 +330,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 +406,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,389 +666,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 && - path.pathBytes.isNotEmpty && - directRepeater.pubkeyFirstByte == path.pathBytes.first; - final isSecondDirectRepeater = - secondDirectRepeater != null && - path.pathBytes.isNotEmpty && - secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - final isThirdDirectRepeater = - thirdDirectRepeater != null && - path.pathBytes.isNotEmpty && - thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - - 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, - ); - final pathLength = path.pathBytes.length; - - // Set the path override to persist user's choice - await connector.setPathOverride( - _resolveContact(connector), - pathLen: pathLength, - 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.formatPathHex(pathBytes); - final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts); - - 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; @@ -1202,29 +736,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); @@ -1439,7 +950,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)), ], @@ -1480,72 +996,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, - ); - - 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', - ); - await connector.setPathOverride( - _resolveContact(connector), - pathLen: result.length, - pathBytes: result, - ); - appLogger.info('setPathOverride completed', tag: 'ChatScreen'); - - if (!mounted) return; - await _notifyPathSet( - connector, - _resolveContact(connector), - result, - result.length, - ); - } - void _openMessagePath(Message message, Contact contact) { final connector = context.read(); final fourByteHex = message.fourByteRoomContactKey @@ -1605,15 +1055,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), @@ -1646,14 +1095,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), @@ -1673,10 +1114,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); + }, ), ], ), @@ -1703,8 +1154,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), @@ -1748,7 +1198,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; @@ -1759,7 +1208,6 @@ class _MessageBubble extends StatelessWidget { required this.senderName, required this.sourceId, required this.textScale, - this.onTap, this.onLongPress, this.onRetryReaction, }); @@ -1801,8 +1249,8 @@ class _MessageBubble extends StatelessWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: PlatformInfo.isDesktop ? null : onTap, + LayoutBuilder( + builder: (context, constraints) => GestureDetector( onLongPress: onLongPress, onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress?.call() @@ -1826,11 +1274,14 @@ class _MessageBubble extends StatelessWidget { vertical: 8, ), constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, + 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, @@ -1863,20 +1314,6 @@ class _MessageBubble extends StatelessWidget { 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, - ), - ) - : null, ) else if (gifId != null) Stack( @@ -1892,30 +1329,6 @@ class _MessageBubble extends StatelessWidget { ), ), ), - 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 @@ -1937,94 +1350,95 @@ class _MessageBubble extends StatelessWidget { ), ), ), - if (!enableTracing && isOutgoing) ...[ + ], + ), + if (enableTracing && + isOutgoing && + message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + context + .read() + .settings + .maxMessageRetries, + ), + style: TextStyle( + fontSize: 10 * textScale, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 10 * textScale, + color: metaColor, + ), + ), + if (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, + MessageStatusIcon( + size: 12 * textScale, + onColor: metaColor, + isAcked: + message.status == + MessageStatus.delivered, + isPending: + message.status == MessageStatus.pending, + isFailed: + message.status == MessageStatus.failed, + ), + ], + if (enableTracing && + message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10 * textScale, + color: isOutgoing + ? metaColor + : Theme.of( + context, + ).colorScheme.tertiary, + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9 * textScale, + color: isOutgoing + ? metaColor + : Theme.of( + context, + ).colorScheme.tertiary, ), ), ], ], ), - if (enableTracing) ...[ - if (isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount( - message.retryCount, - context - .read() - .settings - .maxMessageRetries, - ), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, - ), - ), - if (isOutgoing) ...[ - const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == - MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', - style: TextStyle( - fontSize: 9, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - ), - ], - ], - ), - ), - ], + ), ], ), ), @@ -2032,6 +1446,7 @@ class _MessageBubble extends StatelessWidget { ], ), ), + ), if (message.reactions.isNotEmpty) ...[ const SizedBox(height: 4), Padding( @@ -2059,9 +1474,9 @@ 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, @@ -2184,8 +1599,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, @@ -2201,56 +1616,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..53271d8f 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,19 @@ 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 bdadc2b6..d64b8dd1 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,16 @@ 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 +847,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 +882,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 +922,7 @@ class _ContactsScreenState extends State ); }, ), - ), + ), ), ], ); @@ -1048,7 +1143,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), ), ), ], @@ -1359,14 +1454,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( @@ -1403,10 +1490,15 @@ 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); @@ -1441,7 +1533,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), ), ), ], @@ -1473,25 +1565,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. @@ -1502,7 +1583,7 @@ class _ContactTile extends StatelessWidget { ), ), child: SizedBox( - width: 120, + width: 96, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, @@ -1516,7 +1597,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, @@ -1529,7 +1613,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), ), ], ), @@ -1548,37 +1634,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..736e8f36 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,17 @@ 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 +226,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 +245,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,35 +444,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(); diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index f908f5ea..1d63b910 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,7 @@ 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 +517,18 @@ 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 +631,7 @@ 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 +700,7 @@ 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 +719,14 @@ 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 +734,7 @@ 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 +742,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 +758,7 @@ 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 +1738,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..6bc62018 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,7 @@ 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 766b1852..dd6462b7 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -183,17 +183,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), ), ], @@ -417,61 +417,76 @@ 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), ], @@ -906,8 +921,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) @@ -919,26 +934,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, + ), ), ), ), @@ -1030,39 +1047,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, + ), + ), ), ), ); @@ -1208,7 +1223,7 @@ class _MapScreenState extends State { Icon( Icons.location_on, size: 16, - color: Colors.grey, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), Text( ": $nodeCount", @@ -1221,10 +1236,10 @@ 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}", @@ -1237,10 +1252,10 @@ 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", @@ -1536,56 +1551,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); @@ -1593,7 +1566,7 @@ class _MapScreenState extends State { final unread = connector.getUnreadCountForContactKey( contact.publicKeyHex, ); - Navigator.pop(dialogContext); + Navigator.pop(sheetContext); Navigator.push( context, MaterialPageRoute( @@ -1606,30 +1579,88 @@ 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: 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), + ), + ], + ), + ), + ); + }, ); } @@ -1714,6 +1745,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); @@ -1745,7 +1779,7 @@ class _MapScreenState extends State { label, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), @@ -1810,9 +1844,8 @@ class _MapScreenState extends State { ); await connector.refreshDeviceInfo(); if (!mounted) return; - showDismissibleSnackBar( - messenger.context, - content: Text(successMsg), + messenger.showSnackBar( + SnackBar(content: Text(successMsg)), ); }, ), @@ -2202,7 +2235,7 @@ 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), @@ -2354,7 +2387,7 @@ 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( _pathTrace diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 77559d48..e92a5cc0 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,11 +326,9 @@ 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 && @@ -435,7 +379,7 @@ class _NeighborsScreenState extends State { fmtDuration(entry.value['lastHeard'] + 0.0), ), entry.value['snr'], - connector.currentSf!, + connector.currentSf, ), ], ), @@ -447,7 +391,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 c810570d..81082b9e 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -103,6 +103,9 @@ class _PathTraceMapScreenState extends State { double _pathDistanceMeters = 0.0; bool _showNodeLabels = true; Contact? _targetContact; + // 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) { return pathBytes @@ -168,17 +171,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, ), ], @@ -228,6 +231,23 @@ class _PathTraceMapScreenState extends State { return 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(() { @@ -236,9 +256,13 @@ class _PathTraceMapScreenState extends State { }); } + final connector = Provider.of(context, listen: false); + final livePath = _resolveLivePath(connector); + _tracedPath = livePath; + final pathTmp = widget.reversePathAround - ? Uint8List.fromList(widget.path.reversed.toList()) - : widget.path; + ? Uint8List.fromList(livePath.reversed.toList()) + : livePath; final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; @@ -248,7 +272,6 @@ class _PathTraceMapScreenState extends State { noNotify: !mounted, ); - final connector = Provider.of(context, listen: false); final frame = buildTraceReq( DateTime.now().millisecondsSinceEpoch ~/ 1000, 0, //flags @@ -414,13 +437,13 @@ class _PathTraceMapScreenState extends State { final tc = _targetContact!; if (tc.hasLocation) { targetPos = LatLng(tc.latitude!, tc.longitude!); - } else if (widget.path.length > 1) { + } else if (_tracedPath.length > 1) { // 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 lastHop = widget.reversePathAround - ? widget.path.first - : widget.path.last; + ? _tracedPath.first + : _tracedPath.last; final peers = connector.allContacts .where( @@ -593,7 +616,7 @@ 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( @@ -640,31 +663,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 ? label : '~$label', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), - ], - ), - alignment: Alignment.center, - child: Text( - hasGps ? label : '~$label', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, ), ), ), @@ -689,29 +716,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, ), ), ), @@ -735,26 +766,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), ), ), ); @@ -927,6 +962,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, @@ -949,9 +990,26 @@ 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..93720f28 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,16 @@ 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..337d148c 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -72,11 +72,11 @@ 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 +90,12 @@ 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 +105,14 @@ 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 +193,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 +213,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 +231,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 +251,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 +270,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 +329,12 @@ 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..1d8ae3bc 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,7 @@ 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 +669,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 +770,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 +948,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 +961,7 @@ class _RepeaterSettingsScreenState extends State { content: Text( l10n.repeater_settingsPartialFailure(failures.join('; ')), ), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -973,7 +976,7 @@ class _RepeaterSettingsScreenState extends State { content: Text( context.l10n.repeater_errorSavingSettings(e.toString()), ), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -984,6 +987,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 +1081,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 +1120,8 @@ class _RepeaterSettingsScreenState extends State { const SizedBox(height: 16), _buildAdvancedCard(), const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 16), _buildDangerZoneCard(), ], ), @@ -1388,13 +1337,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 +1373,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 +2200,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 +2229,7 @@ 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..e665cbc7 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,24 @@ 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 +175,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 +234,43 @@ 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 +284,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 +315,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 ead36eaf..0fe077cb 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; @@ -307,18 +308,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), @@ -354,13 +343,13 @@ 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( @@ -378,7 +367,7 @@ 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), @@ -565,6 +554,8 @@ class _SettingsScreenState extends State { int.tryParse(customVars["gps_interval"] ?? "") ?? 900; intervalController.text = currentInterval.toString(); + String? intervalError; + showDialog( context: context, builder: (dialogContext) => StatefulBuilder( @@ -600,9 +591,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, @@ -633,24 +630,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; @@ -716,6 +714,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( @@ -735,7 +763,7 @@ class _SettingsScreenState extends State { }, child: Text( l10n.common_reboot, - style: const TextStyle(color: Colors.orange), + style: TextStyle(color: Theme.of(context).colorScheme.tertiary), ), ), ], @@ -1126,6 +1154,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); @@ -1392,7 +1422,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) { @@ -1504,6 +1551,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { content: Text(l10n.settings_error(e.toString())), ); } + if (!mounted) return; Navigator.pop(context); } @@ -1579,6 +1627,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, @@ -1662,6 +1711,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, ), @@ -1683,7 +1733,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..755a4298 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,15 +220,13 @@ 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 +266,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 f277a8e1..6634a6af 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -13,7 +13,7 @@ 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'; @@ -118,7 +118,7 @@ class _TelemetryScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.telemetry_requestTimeout), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } if (isAutoRefreshRequest && _isAutoRefreshEnabled) { @@ -178,7 +178,6 @@ class _TelemetryScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.telemetry_receivedData), - backgroundColor: Colors.green, ); } _statusTimeout?.cancel(); @@ -235,7 +234,7 @@ class _TelemetryScreenState extends State { showDismissibleSnackBar( context, content: Text(context.l10n.telemetry_errorLoading(e.toString())), - backgroundColor: Colors.red, + backgroundColor: Theme.of(context).colorScheme.error, ); } } @@ -323,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( @@ -347,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 @@ -441,7 +385,7 @@ 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) && @@ -718,14 +662,14 @@ class _TelemetryScreenState extends State { alignment: Alignment.center, children: [ Center(child: Text(l10n.common_disable)), - const Positioned( + Positioned( right: 0, child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, - color: Colors.white, + color: Theme.of(context).colorScheme.onPrimary, ), ), ), @@ -971,21 +915,20 @@ class _TelemetryScreenState 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/usb_screen.dart b/lib/screens/usb_screen.dart index 25992de8..331de5e3 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,15 +199,13 @@ 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 +222,11 @@ 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 +238,12 @@ 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 +272,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 +362,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 b367e0e5..d8e70ce3 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -248,7 +248,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, @@ -304,7 +304,8 @@ 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..df27ec68 100644 --- a/lib/widgets/gif_picker.dart +++ b/lib/widgets/gif_picker.dart @@ -180,7 +180,7 @@ 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 +197,11 @@ 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 +219,11 @@ 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..66c1fa4b --- /dev/null +++ b/lib/widgets/path_editor_sheet.dart @@ -0,0 +1,377 @@ +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; + + const PathEditorSheet({ + super.key, + required this.availableContacts, + this.initialPath = const [], + }); + + static Future show( + BuildContext context, { + required List availableContacts, + List initialPath = const [], + }) { + 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, + ), + ), + ), + ); + } + + @override + State createState() => _PathEditorSheetState(); +} + +class _Hop { + final int id; + final int byte; + + const _Hop(this.id, this.byte); +} + +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 byte in widget.initialPath) { + _hops.add(_Hop(_nextHopId++, byte)); + } + _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 = PathHelper.formatPathHex( + _hops.map((h) => h.byte).toList(), + ); + _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 invalid = tokens + .where((t) => t.length != 2 || 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) => _Hop(_nextHopId++, int.parse(t, radix: 16)))); + }); + } + + void _addHop(Contact contact) { + if (_hops.length >= _maxHops) return; + setState(() { + _hops.add(_Hop(_nextHopId++, contact.publicKey.first)); + _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() { + Navigator.pop( + context, + Uint8List.fromList(_hops.map((h) => h.byte).toList()), + ); + } + + Widget _hopTile(BuildContext context, int index) { + final l10n = context.l10n; + final scheme = Theme.of(context).colorScheme; + final hop = _hops[index]; + final hex = PathHelper.hopHex(hop.byte); + final name = PathHelper.hopName(hop.byte, widget.availableContacts); + + 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.hopHex(contact.publicKey.first)}', + ), + 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 a2122f46..00000000 --- a/lib/widgets/path_management_dialog.dart +++ /dev/null @@ -1,510 +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.formatPathHex(pathBytes); - final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts); - - 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, - ); - - if (result != null && context.mounted) { - await connector.setPathOverride( - currentContact, - pathLen: result.length, - pathBytes: result, - ); - - if (!context.mounted) return; - showDismissibleSnackBar( - context, - content: Text(l10n.chat_hopsCount(result.length)), - 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 && - path.pathBytes.isNotEmpty && - directRepeater.pubkeyFirstByte == path.pathBytes.first; - final isSecondDirectRepeater = - secondDirectRepeater != null && - path.pathBytes.isNotEmpty && - secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - final isThirdDirectRepeater = - thirdDirectRepeater != null && - path.pathBytes.isNotEmpty && - thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; - - 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, - ); - final pathLength = path.pathBytes.length; - - await connector.setPathOverride( - currentContact, - pathLen: pathLength, - 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 44ae58f9..00000000 --- a/lib/widgets/path_selection_dialog.dart +++ /dev/null @@ -1,346 +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; - - const PathSelectionDialog({ - super.key, - required this.availableContacts, - required this.title, - this.initialPath, - this.currentPathLabel, - this.onRefresh, - }); - - @override - State createState() => _PathSelectionDialogState(); - - static Future show( - BuildContext context, { - required List availableContacts, - String? title, - String? initialPath, - String? currentPathLabel, - VoidCallback? onRefresh, - }) { - return showDialog( - context: context, - builder: (context) => PathSelectionDialog( - availableContacts: availableContacts, - title: title ?? context.l10n.path_enterCustomPath, - initialPath: initialPath, - currentPathLabel: currentPathLabel, - onRefresh: onRefresh, - ), - ); - } -} - -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 pathParts = _selectedContacts - .map((contact) { - if (contact.publicKeyHex.length >= 2) { - return contact.publicKeyHex.substring(0, 2); - } - 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 = []; - - for (final id in pathIds) { - if (id.length < 2) { - invalidPrefixes.add(id); - continue; - } - - final prefix = id.substring(0, 2); - try { - final byte = int.parse(prefix, radix: 16); - pathBytesList.add(byte); - } 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; - } - - // Check max path length (64 hops) - if (pathBytesList.length > 64) { - 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: 191, // 64 hops * 2 chars + 63 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, 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..9ba8efd9 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,14 @@ 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 +494,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..8a737130 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,14 @@ 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 +423,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..ddb0bb04 --- /dev/null +++ b/lib/widgets/routing_sheet.dart @@ -0,0 +1,709 @@ +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, + ); + if (result == null || !mounted) return; + await connector.setPathOverride( + contact, + pathLen: result.length, + pathBytes: result, + ); + await _verifyPath(connector, contact, result); + } + + Future _applyHistoryPath( + MeshCoreConnector connector, + Contact contact, + PathRecord record, + ) async { + final bytes = Uint8List.fromList(record.pathBytes); + await connector.setPathOverride( + contact, + pathLen: bytes.length, + 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(PathRecord record, List ranked) { + if (record.pathBytes.isNotEmpty) { + final first = record.pathBytes.first; + for (var i = 0; i < ranked.length && i < 3; i++) { + if (ranked[i].pubkeyFirstByte == first) { + 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); + 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); + } + } + + 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.formatPathHex(pathBytes); + final resolvedNames = PathHelper.resolvePathNames( + pathBytes, + connector.allContacts, + ); + + 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) + : 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(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 99f20539..21efffd1 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -166,7 +166,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 index b47d16d1..a73fb640 100644 --- a/lib/widgets/telemetry_location_map.dart +++ b/lib/widgets/telemetry_location_map.dart @@ -147,19 +147,19 @@ class _TelemetryLocationMapState extends State { children: [ _MapButton( icon: Icons.add, - tooltip: 'Zoom in', + tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomBy(1), ), const SizedBox(height: 6), _MapButton( icon: Icons.remove, - tooltip: 'Zoom out', + tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomBy(-1), ), const SizedBox(height: 6), _MapButton( icon: Icons.my_location, - tooltip: 'Center map', + tooltip: context.l10n.map_centerMap, onPressed: () => _mapController.move(_position, _initialZoom), ), diff --git a/pubspec.yaml b/pubspec.yaml index 72cca35f..504d61f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: path_provider: ^2.1.5 share_plus: ^12.0.1 build_pipe: ^0.3.1 - material_symbols_icons: ^4.2906.0 + material_symbols_icons: ^4.2928.1 web: ^1.1.1 flutter_svg: ^2.0.10+1 flutter_blue_plus_platform_interface: ^8.2.1 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..ecd33ee9 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -157,10 +157,18 @@ 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 +194,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()); diff --git a/untranslated.json b/untranslated.json index 9e26dfee..c3a9394b 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,987 @@ -{} \ No newline at end of file +{ + "bg": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "de": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "es": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "fr": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "hu": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "it": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "ja": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "ko": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "nl": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "pl": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "pt": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "ru": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "sk": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "sl": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "sv": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "uk": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ], + + "zh": [ + "common_undo", + "messageStatus_sent", + "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", + "routing_neverWorked", + "routing_deliveryCounts", + "routing_floodDelivery", + "pathEditor_title", + "pathEditor_hopCounter", + "pathEditor_noHops", + "pathEditor_addHops", + "pathEditor_searchRepeaters", + "pathEditor_advancedHex", + "pathEditor_hexLabel", + "pathEditor_hexHelper", + "pathEditor_invalidTokens", + "pathEditor_tooManyHops", + "pathEditor_usePath", + "pathEditor_removeHop", + "pathEditor_unknownHop", + "map_zoomIn", + "map_zoomOut", + "map_centerMap", + "chrome_bluetoothRequiresChromium", + "channels_communityShortId", + "pathTrace_legendGpsConfirmed", + "pathTrace_legendInferred" + ] +}