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.
This commit is contained in:
zjs81
2026-06-11 00:07:12 -07:00
parent 743ef7f124
commit cba1e5950c
86 changed files with 8149 additions and 6379 deletions
Submodule .claude/worktrees/agent-a01277dd8cf0dedb6 added at e37616fa15
Submodule .claude/worktrees/agent-a0c7384491c5fc807 added at e37616fa15
Submodule .claude/worktrees/agent-a1751d6addbf69e1d added at e37616fa15
Submodule .claude/worktrees/agent-a2fad933ed81b10e2 added at e37616fa15
Submodule .claude/worktrees/agent-a4d771c23db919c7f added at e37616fa15
Submodule .claude/worktrees/agent-a6675128f3ee1d6a6 added at e37616fa15
Submodule .claude/worktrees/agent-a894f10813e684f84 added at e37616fa15
Submodule .claude/worktrees/agent-a8ba0537ad42768d7 added at e37616fa15
Submodule .claude/worktrees/agent-a926d49d72757ce68 added at e37616fa15
Submodule .claude/worktrees/agent-ad2e38b6608f23ab4 added at e37616fa15
Submodule .claude/worktrees/agent-ada5836798aea71a1 added at e37616fa15
Submodule .claude/worktrees/agent-aee10bd582e9e36cf added at e37616fa15
+160 -104
View File
@@ -160,6 +160,7 @@ class MeshCoreConnector extends ChangeNotifier {
{}; // contactPubKeyHex -> Set of "targetHash_emoji" {}; // contactPubKeyHex -> Set of "targetHash_emoji"
StreamSubscription<List<ScanResult>>? _scanSubscription; StreamSubscription<List<ScanResult>>? _scanSubscription;
StreamSubscription<bool>? _isScanningSubscription;
StreamSubscription<BluetoothConnectionState>? _connectionSubscription; StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
StreamSubscription<List<int>>? _notifySubscription; StreamSubscription<List<int>>? _notifySubscription;
Timer? _notifyListenersTimer; Timer? _notifyListenersTimer;
@@ -507,10 +508,22 @@ class MeshCoreConnector extends ChangeNotifier {
if (messages == null) return; if (messages == null) return;
final removed = messages.remove(message); final removed = messages.remove(message);
if (!removed) return; if (!removed) return;
_retryService?.untrack(message.messageId);
await _messageStore.saveMessages(contactKeyHex, messages); await _messageStore.saveMessages(contactKeyHex, messages);
notifyListeners(); notifyListeners();
} }
Future<void> resendMessage(Contact contact, Message message) async {
await deleteMessage(message);
await sendMessage(
contact,
message.text,
originalText: message.originalText,
translatedLanguageCode: message.translatedLanguageCode,
translationModelId: message.translationModelId,
);
}
Future<void> _loadMessagesForContact(String contactKeyHex) async { Future<void> _loadMessagesForContact(String contactKeyHex) async {
if (_loadedConversationKeys.contains(contactKeyHex)) return; if (_loadedConversationKeys.contains(contactKeyHex)) return;
_loadedConversationKeys.add(contactKeyHex); _loadedConversationKeys.add(contactKeyHex);
@@ -1356,6 +1369,21 @@ class MeshCoreConnector extends ChangeNotifier {
}) async { }) async {
if (_state == MeshCoreConnectionState.scanning) return; 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(); _scanResults.clear();
_linuxSystemScanResults.clear(); _linuxSystemScanResults.clear();
_setState(MeshCoreConnectionState.scanning); _setState(MeshCoreConnectionState.scanning);
@@ -1409,20 +1437,40 @@ class MeshCoreConnector extends ChangeNotifier {
}); });
try { 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( await FlutterBluePlus.startScan(
withKeywords: MeshCoreUuids.deviceNamePrefixes, withServices: [Guid(MeshCoreUuids.service)],
webOptionalServices: [Guid(MeshCoreUuids.service)], webOptionalServices: [Guid(MeshCoreUuids.service)],
timeout: timeout, timeout: timeout,
androidScanMode: AndroidScanMode.lowLatency, androidScanMode: AndroidScanMode.lowLatency,
); );
} catch (error) { } catch (error) {
_appDebugLogService?.warn('Scan/picker failure: $error', tag: 'BLE Scan'); _appDebugLogService?.warn('Scan/picker failure: $error', tag: 'BLE Scan');
_setState(MeshCoreConnectionState.disconnected); await stopScan();
rethrow; rethrow;
} }
await Future.delayed(timeout); // Reset our shared state when the native scan ends — whether it was stopped
await stopScan(); // by the user (stopScan), by the platform timeout, or by Bluetooth turning
// off. This replaces a blocking `Future.delayed(timeout)` tail that kept
// startScan() pending for the whole timeout and made Stop appear ineffective.
// `isScanning` is a re-emit stream that replays its latest value on listen,
// so skip(1) to ignore that and only react to a genuine transition to false.
await _isScanningSubscription?.cancel();
_isScanningSubscription = FlutterBluePlus.isScanning.skip(1).listen((
scanning,
) {
if (!scanning && _state == MeshCoreConnectionState.scanning) {
unawaited(stopScan());
}
});
} }
Future<void> _loadLinuxSystemDevicesForScan() async { Future<void> _loadLinuxSystemDevicesForScan() async {
@@ -1430,15 +1478,13 @@ class MeshCoreConnector extends ChangeNotifier {
final systemDevices = await FlutterBluePlus.systemDevices([ final systemDevices = await FlutterBluePlus.systemDevices([
Guid(MeshCoreUuids.service), 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 _linuxSystemScanResults
..clear() ..clear()
..addAll( ..addAll(
systemDevices systemDevices
.where(
(device) => MeshCoreUuids.deviceNamePrefixes.any(
device.platformName.startsWith,
),
)
.map( .map(
(device) => ScanResult( (device) => ScanResult(
device: device, device: device,
@@ -1499,9 +1545,17 @@ class MeshCoreConnector extends ChangeNotifier {
} }
await _scanSubscription?.cancel(); await _scanSubscription?.cancel();
_scanSubscription = null; _scanSubscription = null;
await _isScanningSubscription?.cancel();
_isScanningSubscription = null;
if (_state == MeshCoreConnectionState.scanning) { 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; activeTransport == MeshCoreTransportType.tcp;
} }
/// Fast (non-timeout) connect failures are usually a stale link left over
/// from a previous session and recover on an immediate retry. Timeouts mean
/// the device is likely off or out of range, so retrying would only delay
/// genuine failure feedback.
@visibleForTesting
static bool shouldRetryBleConnectAfterError(String errorText) {
final lowerErrorText = errorText.toLowerCase();
return !lowerErrorText.contains('timed out') &&
!lowerErrorText.contains('timeout');
}
Future<void> connect( Future<void> connect(
BluetoothDevice device, { BluetoothDevice device, {
String? displayName, String? displayName,
@@ -1911,19 +1976,72 @@ class MeshCoreConnector extends ChangeNotifier {
} }
} }
} else { } else {
try { Future<void> attemptConnect() {
await device.connect( return device.connect(
timeout: connectTimeout, timeout: connectTimeout,
mtu: null, mtu: null,
license: License.free, 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) { } catch (error) {
_appDebugLogService?.error( _appDebugLogService?.error(
'device.connect() failure: $error', 'device.connect() failure: $error',
tag: 'BLE Connect', tag: 'BLE Connect',
); );
if (PlatformInfo.isWeb ||
!shouldRetryBleConnectAfterError(error.toString())) {
rethrow; rethrow;
} }
// Fast (non-timeout) failures are usually a stale connection left by
// a previous session; clean up and retry once before surfacing.
_appDebugLogService?.warn(
'Retrying connect once after clearing possible stale connection',
tag: 'BLE Connect',
);
try {
await device.disconnect(queue: false);
} catch (cleanupError) {
_appDebugLogService?.warn(
'Pre-retry cleanup disconnect failed (continuing): $cleanupError',
tag: 'BLE Connect',
);
}
await Future<void>.delayed(const Duration(milliseconds: 500));
try {
await attemptConnect();
_appDebugLogService?.info(
'Retry connect succeeded after stale-connection cleanup',
tag: 'BLE Connect',
);
} catch (retryError) {
_appDebugLogService?.error(
'device.connect() retry failure: $retryError',
tag: 'BLE Connect',
);
rethrow;
}
}
} }
if (PlatformInfo.isLinux) { if (PlatformInfo.isLinux) {
@@ -3237,6 +3355,7 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildRemoveContactFrame(contact.publicKey)); await sendFrame(buildRemoveContactFrame(contact.publicKey));
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex); _contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
_knownContactKeys.remove(contact.publicKeyHex); _knownContactKeys.remove(contact.publicKeyHex);
unawaited(updateKnownDiscovered());
unawaited(_persistContacts()); unawaited(_persistContacts());
_conversations.remove(contact.publicKeyHex); _conversations.remove(contact.publicKeyHex);
_loadedConversationKeys.remove(contact.publicKeyHex); _loadedConversationKeys.remove(contact.publicKeyHex);
@@ -3450,12 +3569,10 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> sendCliCommand(String command) async { Future<void> sendCliCommand(String command) async {
if (!isConnected) return; if (!isConnected) return;
final selfKey = _selfPublicKey;
// CLI commands are sent as UTF-8 text with a special prefix if (selfKey == null) return;
final commandBytes = utf8.encode(command);
final bytes = Uint8List.fromList([0x01, ...commandBytes, 0x00]);
_lastSentWasCliCommand = true; _lastSentWasCliCommand = true;
await sendFrame(bytes); await sendFrame(buildSendCliCommandFrame(selfKey, command));
} }
Future<void> setNodeName(String name) async { Future<void> setNodeName(String name) async {
@@ -3929,8 +4046,8 @@ class MeshCoreConnector extends ChangeNotifier {
_advertLocPolicy = reader.readByte(); _advertLocPolicy = reader.readByte();
final telemetryFlag = reader.readByte(); final telemetryFlag = reader.readByte();
_telemetryModeBase = telemetryFlag & 0x03; _telemetryModeBase = telemetryFlag & 0x03;
_telemetryModeEnv = telemetryFlag >> 2 & 0x03; _telemetryModeLoc = telemetryFlag >> 2 & 0x03;
_telemetryModeLoc = telemetryFlag >> 4 & 0x03; _telemetryModeEnv = telemetryFlag >> 4 & 0x03;
_manualAddContacts = reader.readByte() & 0x01 == 0x00; _manualAddContacts = reader.readByte() & 0x01 == 0x00;
@@ -4275,7 +4392,14 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'Connector', tag: 'Connector',
); );
notifyListeners(); notifyListeners();
removeContact(contactTmp); unawaited(
removeContact(contactTmp).catchError(
(e) => appLogger.warn(
'Failed to remove self contact: $e',
tag: 'Connector',
),
),
);
return; return;
} }
final contact = getFromDiscovered(contactTmp); final contact = getFromDiscovered(contactTmp);
@@ -5521,6 +5645,9 @@ class MeshCoreConnector extends ChangeNotifier {
} }
messages.add(message); messages.add(message);
if (messages.length > _messageWindowSize) {
messages.removeRange(0, messages.length - _messageWindowSize);
}
_messageStore.saveMessages(pubKeyHex, messages); _messageStore.saveMessages(pubKeyHex, messages);
notifyListeners(); notifyListeners();
} }
@@ -5907,20 +6034,24 @@ class MeshCoreConnector extends ChangeNotifier {
bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) { bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) {
if (existing.text != incoming.text) return false; 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 = final diffMs =
(existing.timestamp.millisecondsSinceEpoch - (existing.timestamp.millisecondsSinceEpoch -
incoming.timestamp.millisecondsSinceEpoch) incoming.timestamp.millisecondsSinceEpoch)
.abs(); .abs();
if (diffMs > 30000) return false; if (diffMs > windowMs) return false;
if (existing.senderName == incoming.senderName) return true; if (existing.senderName == incoming.senderName) return true;
if (isSelfEcho) return true;
if (existing.isOutgoing && !incoming.isOutgoing) {
final selfName = _selfName ?? 'Me';
if (incoming.senderName == selfName || existing.senderName == selfName) {
return true;
}
}
return false; return false;
} }
@@ -6152,6 +6283,7 @@ class MeshCoreConnector extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_scanSubscription?.cancel(); _scanSubscription?.cancel();
_isScanningSubscription?.cancel();
_connectionSubscription?.cancel(); _connectionSubscription?.cancel();
_usbFrameSubscription?.cancel(); _usbFrameSubscription?.cancel();
_notifySubscription?.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) { bool hasValidLocation(double? latitude, double? longitude) {
const double epsilon = 1e-6; const double epsilon = 1e-6;
final lat = latitude ?? 0.0; final lat = latitude ?? 0.0;
+6 -1
View File
@@ -457,8 +457,13 @@ String pubKeyToHex(Uint8List pubKey) {
// Helper to convert hex string to public key // Helper to convert hex string to public key
Uint8List hexToPubKey(String hex) { 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); 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); result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
} }
return result; return result;
+4
View File
@@ -3,6 +3,10 @@ class MeshCoreUuids {
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
/// Known advertised-name prefixes used by stock MeshCore firmware builds.
/// Discovery no longer filters on these (it filters on the [service] UUID so
/// that community forks with custom names are still found); kept for
/// reference and possible future display heuristics.
static const List<String> deviceNamePrefixes = [ static const List<String> deviceNamePrefixes = [
"MeshCore-", "MeshCore-",
"Whisper-", "Whisper-",
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_protocol.dart';
import '../utils/emoji_utils.dart';
IconData contactTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color contactTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
}
Color colorForName(String name) {
const colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.amber,
Colors.deepOrange,
];
return colors[name.hashCode.abs() % colors.length];
}
String firstCharacterOrEmoji(String name) {
if (name.isEmpty) return '?';
final emoji = firstEmoji(name);
if (emoji != null) return emoji;
final runes = name.runes.toList();
if (runes.isEmpty) return '?';
return String.fromCharCode(runes[0]).toUpperCase();
}
+18 -13
View File
@@ -8,24 +8,29 @@ class PathHelper {
.join(','); .join(',');
} }
static String hopHex(int byte) {
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
}
static String? hopName(int byte, List<Contact> 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( static String resolvePathNames(
List<int> pathBytes, List<int> pathBytes,
List<Contact> allContacts, List<Contact> allContacts,
) { ) {
return pathBytes return pathBytes
.map((b) { .map((b) => hopName(b, allContacts) ?? hopHex(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(' | ');
})
.join(' \u2192 '); .join(' \u2192 ');
} }
} }
+89 -73
View File
@@ -28,6 +28,12 @@
"common_remove": "Remove", "common_remove": "Remove",
"common_enable": "Enable", "common_enable": "Enable",
"common_disable": "Disable", "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_reboot": "Reboot",
"common_loading": "Loading...", "common_loading": "Loading...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -297,17 +303,6 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery", "appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
"appSettings_maxMessageRetries": "Max Message Retries", "appSettings_maxMessageRetries": "Max Message Retries",
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed", "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_battery": "Battery",
"appSettings_batteryChemistry": "Battery Chemistry", "appSettings_batteryChemistry": "Battery Chemistry",
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})", "appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
@@ -453,6 +448,9 @@
} }
}, },
"contacts_newGroup": "New Group", "contacts_newGroup": "New Group",
"contacts_moreOptions": "More options",
"contacts_searchOpen": "Search contacts",
"contacts_searchClose": "Close search",
"contacts_groupName": "Group name", "contacts_groupName": "Group name",
"contacts_groupNameRequired": "Group name is required", "contacts_groupNameRequired": "Group name is required",
"contacts_groupNameReserved": "This group name is reserved", "contacts_groupNameReserved": "This group name is reserved",
@@ -777,15 +775,6 @@
} }
}, },
"debugFrame_hexDump": "Hex Dump:", "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": "{count} {count, plural, =1{hop} other{hops}}",
"@chat_hopsCount": { "@chat_hopsCount": {
"placeholders": { "placeholders": {
@@ -794,31 +783,80 @@
} }
} }
}, },
"chat_successes": "successes",
"chat_score": "Score",
"chat_removePath": "Remove path", "chat_removePath": "Remove path",
"chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.", "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_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_fullPath": "Full Path",
"chat_pathDetailsNotAvailable": "Path details not available yet. Try sending a message to refresh.", "routing_title": "Routing",
"chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "routing_modeAuto": "Auto",
"@chat_pathSetHops": { "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": { "placeholders": {
"hopCount": { "when": {
"type": "int"
},
"status": {
"type": "String" "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_pathSavedLocally": "Saved locally. Connect to sync.",
"chat_pathDeviceConfirmed": "Device confirmed.", "chat_pathDeviceConfirmed": "Device confirmed.",
"chat_pathDeviceNotConfirmed": "Device not confirmed yet.", "chat_pathDeviceNotConfirmed": "Device not confirmed yet.",
@@ -1100,41 +1138,8 @@
"login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.", "login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.",
"common_reload": "Reload", "common_reload": "Reload",
"common_clear": "Clear", "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_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_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", "repeater_management": "Repeater Management",
"room_management": "Room Server Management", "room_management": "Room Server Management",
"repeater_guest": "Repeater Information", "repeater_guest": "Repeater Information",
@@ -1161,9 +1166,6 @@
}, },
"repeater_statusTitle": "Repeater Status", "repeater_statusTitle": "Repeater Status",
"repeater_routingMode": "Routing mode", "repeater_routingMode": "Routing mode",
"repeater_autoUseSavedPath": "Auto (use saved path)",
"repeater_forceFloodMode": "Force Flood Mode",
"repeater_pathManagement": "Path management",
"repeater_refresh": "Refresh", "repeater_refresh": "Refresh",
"repeater_statusRequestTimeout": "Status request timed out.", "repeater_statusRequestTimeout": "Status request timed out.",
"repeater_errorLoadingStatus": "Error loading status: {error}", "repeater_errorLoadingStatus": "Error loading status: {error}",
@@ -2485,5 +2487,19 @@
"contact_typeRepeater": "Repeater", "contact_typeRepeater": "Repeater",
"contact_typeRoom": "Room", "contact_typeRoom": "Room",
"contact_typeSensor": "Sensor", "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"
} }
+324 -210
View File
@@ -298,6 +298,42 @@ abstract class AppLocalizations {
/// **'Disable'** /// **'Disable'**
String get common_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. /// No description provided for @common_reboot.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1534,12 +1570,6 @@ abstract class AppLocalizations {
/// **'Number of retry attempts before marking a message as failed'** /// **'Number of retry attempts before marking a message as failed'**
String get appSettings_maxMessageRetriesSubtitle; 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. /// No description provided for @appSettings_battery.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1882,6 +1912,24 @@ abstract class AppLocalizations {
/// **'New Group'** /// **'New Group'**
String get contacts_newGroup; 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. /// No description provided for @contacts_groupName.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2710,78 +2758,12 @@ abstract class AppLocalizations {
/// **'Hex Dump:'** /// **'Hex Dump:'**
String get debugFrame_hexDump; 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. /// No description provided for @chat_hopsCount.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'{count} {count, plural, =1{hop} other{hops}}'** /// **'{count} {count, plural, =1{hop} other{hops}}'**
String chat_hopsCount(int count); 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. /// No description provided for @chat_removePath.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2794,71 +2776,251 @@ abstract class AppLocalizations {
/// **'No path history yet.\nSend a message to discover paths.'** /// **'No path history yet.\nSend a message to discover paths.'**
String get chat_noPathHistoryYet; 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. /// No description provided for @chat_pathCleared.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Path cleared. Next message will rediscover route.'** /// **'Path cleared. Next message will rediscover route.'**
String get chat_pathCleared; 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. /// No description provided for @chat_fullPath.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Full Path'** /// **'Full Path'**
String get chat_fullPath; String get chat_fullPath;
/// No description provided for @chat_pathDetailsNotAvailable. /// No description provided for @routing_title.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Path details not available yet. Try sending a message to refresh.'** /// **'Routing'**
String get chat_pathDetailsNotAvailable; String get routing_title;
/// No description provided for @chat_pathSetHops. /// No description provided for @routing_modeAuto.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}'** /// **'Auto'**
String chat_pathSetHops(int hopCount, String status); 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. /// No description provided for @chat_pathSavedLocally.
/// ///
@@ -3687,90 +3849,18 @@ abstract class AppLocalizations {
/// **'Clear'** /// **'Clear'**
String get common_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. /// No description provided for @path_currentPathLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Current path'** /// **'Current path'**
String get path_currentPathLabel; 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. /// No description provided for @path_noRepeatersFound.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'No repeaters or room servers found.'** /// **'No repeaters or room servers found.'**
String get path_noRepeatersFound; 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. /// No description provided for @repeater_management.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -3891,24 +3981,6 @@ abstract class AppLocalizations {
/// **'Routing mode'** /// **'Routing mode'**
String get repeater_routingMode; 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. /// No description provided for @repeater_refresh.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -7587,6 +7659,48 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Unknown'** /// **'Unknown'**
String get contact_typeUnknown; 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 class _AppLocalizationsDelegate
+177 -137
View File
@@ -92,6 +92,24 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get common_disable => 'Деактивирай'; 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 @override
String get common_reboot => 'Рестартирай'; String get common_reboot => 'Рестартирай';
@@ -796,11 +814,6 @@ class AppLocalizationsBg extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.'; 'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => 'Батерия'; String get appSettings_battery => 'Батерия';
@@ -1002,6 +1015,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Нова група'; 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 @override
String get contacts_groupName => 'Група'; String get contacts_groupName => 'Група';
@@ -1482,35 +1504,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Хексадесетичен Dump:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1522,12 +1515,6 @@ class AppLocalizationsBg extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Успехи';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Премахни пътя'; String get chat_removePath => 'Премахни пътя';
@@ -1535,52 +1522,144 @@ class AppLocalizationsBg extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.'; 'Няма история на пътищата още.\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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Пътят е почистен. Следващото съобщение ще открие маршрута отново.'; 'Пътят е почистен. Следващото съобщение ще открие маршрута отново.';
@override
String get chat_floodModeSubtitle =>
'Използвайте превключвателя за маршрутизиране в лентата на приложението.';
@override
String get chat_floodModeEnabled =>
'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.';
@override @override
String get chat_fullPath => 'Пълен път'; String get chat_fullPath => 'Пълен път';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Пътят е зададен: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Запазено локално. Свържете се за синхронизиране.'; 'Запазено локално. Свържете се за синхронизиране.';
@@ -2050,66 +2129,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get common_clear => 'Изчисти'; 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 @override
String get path_currentPathLabel => 'Текущ път'; 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 @override
String get path_noRepeatersFound => 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 @override
String get repeater_management => 'Управление на повторители'; String get repeater_management => 'Управление на повторители';
@@ -2176,16 +2202,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Режим на маршрутизиране'; String get repeater_routingMode => 'Режим на маршрутизиране';
@override
String get repeater_autoUseSavedPath =>
'Автоматично (използвай запазения път)';
@override
String get repeater_forceFloodMode => 'Принуди режим на наводняване';
@override
String get repeater_pathManagement => 'Управление на пътища';
@override @override
String get repeater_refresh => 'Презареди'; String get repeater_refresh => 'Презареди';
@@ -4437,4 +4453,28 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get common_disable => 'Deaktivieren'; 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 @override
String get common_reboot => 'Neustart'; String get common_reboot => 'Neustart';
@@ -792,11 +810,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.'; '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 @override
String get appSettings_battery => 'Akku'; String get appSettings_battery => 'Akku';
@@ -998,6 +1011,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Neue Gruppe'; 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 @override
String get contacts_groupName => 'Gruppenname'; String get contacts_groupName => 'Gruppenname';
@@ -1480,36 +1502,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Hex-Dump:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1521,12 +1513,6 @@ class AppLocalizationsDe extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Erfolgreich';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Pfad entfernen'; String get chat_removePath => 'Pfad entfernen';
@@ -1534,51 +1520,144 @@ class AppLocalizationsDe extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.'; '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 @override
String get chat_fullPath => 'Vollständiger Pfad'; String get chat_fullPath => 'Vollständiger Pfad';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Pfad gesetzt: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.'; 'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.';
@@ -2049,65 +2128,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get common_clear => 'Löschen'; 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 @override
String get path_currentPathLabel => 'Aktueller Pfad'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Keine Repeater oder Raumserver gefunden.'; '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 @override
String get repeater_management => 'Repeater-Verwaltung'; String get repeater_management => 'Repeater-Verwaltung';
@@ -2172,16 +2199,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Routenmodus'; 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 @override
String get repeater_refresh => 'Aktualisieren'; String get repeater_refresh => 'Aktualisieren';
@@ -4455,4 +4472,28 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -132
View File
@@ -92,6 +92,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get common_disable => 'Disable'; 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 @override
String get common_reboot => 'Reboot'; String get common_reboot => 'Reboot';
@@ -777,11 +795,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Number of retry attempts before marking a message as failed'; 'Number of retry attempts before marking a message as failed';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => 'Battery'; String get appSettings_battery => 'Battery';
@@ -981,6 +994,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contacts_newGroup => 'New Group'; 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 @override
String get contacts_groupName => 'Group name'; String get contacts_groupName => 'Group name';
@@ -1452,34 +1474,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Hex Dump:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1491,12 +1485,6 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'successes';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Remove path'; String get chat_removePath => 'Remove path';
@@ -1504,50 +1492,144 @@ class AppLocalizationsEn extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'No path history yet.\nSend a message to discover paths.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Path cleared. Next message will rediscover route.'; '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 @override
String get chat_fullPath => 'Full Path'; String get chat_fullPath => 'Full Path';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Path details not available yet. Try sending a message to refresh.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Path set: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => 'Saved locally. Connect to sync.'; String get chat_pathSavedLocally => 'Saved locally. Connect to sync.';
@@ -2009,64 +2091,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get common_clear => 'Clear'; 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 @override
String get path_currentPathLabel => 'Current path'; 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 @override
String get path_noRepeatersFound => 'No repeaters or room servers found.'; 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 @override
String get repeater_management => 'Repeater Management'; String get repeater_management => 'Repeater Management';
@@ -2130,15 +2160,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Routing mode'; 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 @override
String get repeater_refresh => 'Refresh'; String get repeater_refresh => 'Refresh';
@@ -4360,4 +4381,28 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get common_disable => 'Desactivar'; 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 @override
String get common_reboot => 'Reiniciar'; String get common_reboot => 'Reiniciar';
@@ -791,11 +809,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Número de intentos de reintento antes de marcar un mensaje como fallido.'; '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 @override
String get appSettings_battery => 'Batería'; String get appSettings_battery => 'Batería';
@@ -997,6 +1010,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Nuevo Grupo'; 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 @override
String get contacts_groupName => 'Nombre del grupo'; String get contacts_groupName => 'Nombre del grupo';
@@ -1479,34 +1501,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Mapeo Hexadecimal:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1518,12 +1512,6 @@ class AppLocalizationsEs extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Éxitos';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Eliminar ruta'; String get chat_removePath => 'Eliminar ruta';
@@ -1531,53 +1519,144 @@ class AppLocalizationsEs extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.'; '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 @override
String get chat_fullPath => 'Ruta completa'; String get chat_fullPath => 'Ruta completa';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Ruta establecida: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Guardado localmente. Conéctate para sincronizar.'; 'Guardado localmente. Conéctate para sincronizar.';
@@ -2045,66 +2124,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get common_clear => 'Borrar'; 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 @override
String get path_currentPathLabel => 'Ruta actual'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'No se encontraron repetidores ni servidores de sala.'; '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 @override
String get repeater_management => 'Gestión de Repetidores'; String get repeater_management => 'Gestión de Repetidores';
@@ -2169,15 +2195,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Modo de enrutamiento'; 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 @override
String get repeater_refresh => 'Actualizar'; String get repeater_refresh => 'Actualizar';
@@ -4442,4 +4459,28 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -138
View File
@@ -92,6 +92,24 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get common_disable => 'Désactiver'; 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 @override
String get common_reboot => 'Redémarrer'; String get common_reboot => 'Redémarrer';
@@ -797,11 +815,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.'; '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 @override
String get appSettings_battery => 'Batterie'; String get appSettings_battery => 'Batterie';
@@ -1003,6 +1016,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Nouveau Groupe'; 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 @override
String get contacts_groupName => 'Nom du groupe'; String get contacts_groupName => 'Nom du groupe';
@@ -1485,35 +1507,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1525,12 +1518,6 @@ class AppLocalizationsFr extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Succès';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Supprimer le chemin'; String get chat_removePath => 'Supprimer le chemin';
@@ -1538,53 +1525,144 @@ class AppLocalizationsFr extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Le chemin est dégagé. Le prochain message redécouvrira le tracé.'; '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 @override
String get chat_fullPath => 'Chemin complet'; String get chat_fullPath => 'Chemin complet';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Chemin défini : $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Sauvegardé localement. Connectez-vous pour synchroniser.'; 'Sauvegardé localement. Connectez-vous pour synchroniser.';
@@ -2056,66 +2134,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get common_clear => 'Effacer'; 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 @override
String get path_currentPathLabel => 'Chemin actuel'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Aucun répéteur ou room server n\'a été trouvé.'; '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 @override
String get repeater_management => 'Gestion des répéteurs'; String get repeater_management => 'Gestion des répéteurs';
@@ -2181,16 +2206,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Mode de routage'; 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 @override
String get repeater_refresh => 'Rafraîchir'; String get repeater_refresh => 'Rafraîchir';
@@ -4471,4 +4486,28 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -139
View File
@@ -92,6 +92,24 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get common_disable => 'Leteteszt'; 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 @override
String get common_reboot => 'Újraindítás'; String get common_reboot => 'Újraindítás';
@@ -795,11 +813,6 @@ class AppLocalizationsHu extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.'; '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 @override
String get appSettings_battery => 'Akku'; String get appSettings_battery => 'Akku';
@@ -1003,6 +1016,15 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Új csoport'; 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 @override
String get contacts_groupName => 'Csoport neve'; String get contacts_groupName => 'Csoport neve';
@@ -1488,36 +1510,6 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Hex-dump:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1529,12 +1521,6 @@ class AppLocalizationsHu extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'sikerek';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Törölje a elérési útvonalat'; String get chat_removePath => 'Törölje a elérési útvonalat';
@@ -1542,52 +1528,144 @@ class AppLocalizationsHu extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.'; 'Ú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 @override
String get chat_fullPath => 'Teljes elérési út'; String get chat_fullPath => 'Teljes elérési út';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'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.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Path set: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.'; 'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.';
@@ -2058,67 +2136,13 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get common_clear => 'Egyértelmű'; 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 @override
String get path_currentPathLabel => 'Jelenlegi útvonal'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.'; '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 @override
String get repeater_management => 'Adatkapcsolás kezelése'; String get repeater_management => 'Adatkapcsolás kezelése';
@@ -2184,16 +2208,6 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Útvonal-kezelési mód'; 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 @override
String get repeater_refresh => 'Újrafriszol'; String get repeater_refresh => 'Újrafriszol';
@@ -4458,4 +4472,28 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get common_disable => 'Disattivare'; 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 @override
String get common_reboot => 'Riavvia'; String get common_reboot => 'Riavvia';
@@ -794,11 +812,6 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Numero di tentativi di riprova prima di considerare un messaggio come fallito.'; 'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => 'Batteria'; String get appSettings_battery => 'Batteria';
@@ -999,6 +1012,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Nuovo Gruppo'; 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 @override
String get contacts_groupName => 'Nome gruppo'; String get contacts_groupName => 'Nome gruppo';
@@ -1481,34 +1503,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Dumpa Esadecimale:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1520,12 +1514,6 @@ class AppLocalizationsIt extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'successi';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Rimuovi percorso'; String get chat_removePath => 'Rimuovi percorso';
@@ -1533,53 +1521,144 @@ class AppLocalizationsIt extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; '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 @override
String get chat_fullPath => 'Percorso Completo'; String get chat_fullPath => 'Percorso Completo';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Percorso impostato: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Salvatato localmente. Connetti per sincronizzare.'; 'Salvatato localmente. Connetti per sincronizzare.';
@@ -2047,66 +2126,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get common_clear => 'Cancella'; 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 @override
String get path_currentPathLabel => 'Percorso corrente'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Non sono stati trovati ripetitori o server di stanza.'; '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 @override
String get repeater_management => 'Gestione Ripetitori'; String get repeater_management => 'Gestione Ripetitori';
@@ -2173,15 +2199,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Modalità di routing'; 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 @override
String get repeater_refresh => 'Aggiorna'; String get repeater_refresh => 'Aggiorna';
@@ -4447,4 +4464,28 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+247 -199
View File
@@ -92,6 +92,24 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get common_disable => '無効化する'; 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 @override
String get common_reboot => '再起動'; String get common_reboot => '再起動';
@@ -559,7 +577,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get settings_txPowerInvalid => '無効な送信電力 (0-22 dBm)'; String get settings_txPowerInvalid => '無効な送信電力 (0-22 dBm)';
@override @override
String get settings_clientRepeat => 'オフグリッド(電力網から孤立した状態)の繰り返し'; String get settings_clientRepeat => 'オフグリッドリピータ';
@override @override
String get settings_clientRepeatSubtitle => String get settings_clientRepeatSubtitle =>
@@ -709,7 +727,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get appSettings_autoRouteRotation => '自動ルートの切り替え'; String get appSettings_autoRouteRotation => '自動ルートの切り替え';
@override @override
String get appSettings_autoRouteRotationSubtitle => '最適なルートと、洪水モードを切り替える'; String get appSettings_autoRouteRotationSubtitle => '最適なルートと、フラッドモードを切り替える';
@override @override
String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています'; String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています';
@@ -751,11 +769,6 @@ class AppLocalizationsJa extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'メッセージを「失敗」とマークするまでの、再試行回数'; 'メッセージを「失敗」とマークするまでの、再試行回数';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => 'バッテリー'; String get appSettings_battery => 'バッテリー';
@@ -786,7 +799,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get appSettings_showRepeaters => '繰り返し再生機能'; String get appSettings_showRepeaters => '繰り返し再生機能';
@override @override
String get appSettings_showRepeatersSubtitle => '地図上にリピータノードを表示する'; String get appSettings_showRepeatersSubtitle => '地図上にリピータノードを表示する';
@override @override
String get appSettings_showChatNodes => 'チャットノードの表示'; String get appSettings_showChatNodes => 'チャットノードの表示';
@@ -925,7 +938,7 @@ class AppLocalizationsJa extends AppLocalizations {
} }
@override @override
String get contacts_manageRepeater => 'リピータの管理'; String get contacts_manageRepeater => 'リピータの管理';
@override @override
String get contacts_manageRoom => 'ルームサーバーの管理'; String get contacts_manageRoom => 'ルームサーバーの管理';
@@ -950,6 +963,15 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get contacts_newGroup => '新しいグループ'; 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 @override
String get contacts_groupName => 'グループ名'; String get contacts_groupName => 'グループ名';
@@ -1416,33 +1438,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'ヘックスダンプ:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1454,61 +1449,149 @@ class AppLocalizationsJa extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => '成功事例';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'パスを削除する'; String get chat_removePath => 'パスを削除する';
@override @override
String get chat_noPathHistoryYet => 'まだ履歴はありません。\nパスを特定するためにメッセージを送信してください。'; 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 @override
String get chat_pathCleared => '経路が確保されました。次のメッセージでルートを再確認します。'; String get chat_pathCleared => '経路が確保されました。次のメッセージでルートを再確認します。';
@override
String get chat_floodModeSubtitle => 'アプリのバーにあるルーティング切り替え機能を使用する';
@override
String get chat_floodModeEnabled =>
'洪水モードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。';
@override @override
String get chat_fullPath => 'フルパス'; String get chat_fullPath => 'フルパス';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Path set: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => 'ローカルで保存。同期のために接続する。'; String get chat_pathSavedLocally => 'ローカルで保存。同期のために接続する。';
@@ -1531,7 +1614,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する'; String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する';
@override @override
String get chat_floodForced => '洪水(強制的な)'; String get chat_floodForced => 'フラッド(強制的な)';
@override @override
String get chat_directForced => '直接的な(強制的な)'; String get chat_directForced => '直接的な(強制的な)';
@@ -1542,7 +1625,7 @@ class AppLocalizationsJa extends AppLocalizations {
} }
@override @override
String get chat_floodAuto => '洪水 (自動)'; String get chat_floodAuto => 'フラッド (自動)';
@override @override
String get chat_direct => '直接'; String get chat_direct => '直接';
@@ -1607,7 +1690,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get map_chat => 'チャット'; String get map_chat => 'チャット';
@override @override
String get map_repeater => '繰り返し送信装置'; String get map_repeater => 'リピータ';
@override @override
String get map_room => '部屋'; String get map_room => '部屋';
@@ -1702,13 +1785,13 @@ class AppLocalizationsJa extends AppLocalizations {
String get map_chatNodes => 'チャットノード'; String get map_chatNodes => 'チャットノード';
@override @override
String get map_repeaters => '繰り返し送信装置'; String get map_repeaters => 'リピータ';
@override @override
String get map_otherNodes => 'その他のノード'; String get map_otherNodes => 'その他のノード';
@override @override
String get map_showOverlaps => 'リピータキーの重複'; String get map_showOverlaps => 'リピータキーの重複';
@override @override
String get map_keyPrefix => '主要なプレフィックス'; String get map_keyPrefix => '主要なプレフィックス';
@@ -1747,7 +1830,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get map_joinRoom => '部屋に参加する'; String get map_joinRoom => '部屋に参加する';
@override @override
String get map_manageRepeater => 'リピータの管理'; String get map_manageRepeater => 'リピータの管理';
@override @override
String get map_tapToAdd => 'ノードをクリックして、パスに追加します。'; String get map_tapToAdd => 'ノードをクリックして、パスに追加します。';
@@ -1920,7 +2003,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get login_repeaterDescription => String get login_repeaterDescription =>
'設定やステータスにアクセスするために、リピータのパスワードを入力してください。'; '設定やステータスにアクセスするために、リピータのパスワードを入力してください。';
@override @override
String get login_roomDescription => '設定やステータスへのアクセスには、部屋のパスワードを入力してください。'; String get login_roomDescription => '設定やステータスへのアクセスには、部屋のパスワードを入力してください。';
@@ -1935,7 +2018,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get login_autoUseSavedPath => '自動 (保存されたパスを使用)'; String get login_autoUseSavedPath => '自動 (保存されたパスを使用)';
@override @override
String get login_forceFloodMode => '強制的に洪水モードを起動'; String get login_forceFloodMode => '強制的にフラッドモードを起動';
@override @override
String get login_managePaths => 'パスの管理'; String get login_managePaths => 'パスの管理';
@@ -1963,70 +2046,20 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get common_clear => '明確'; 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 @override
String get path_currentPathLabel => '現在の経路'; 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 @override
String get path_noRepeatersFound => '繰り返し機能やルームサーバーは見つかりませんでした。'; String get path_noRepeatersFound => '繰り返し機能やルームサーバーは見つかりませんでした。';
@override @override
String get path_customPathsRequire => 'カスタムパスには、メッセージを中継できる中間地点が必要です。'; String get repeater_management => 'リピータ管理';
@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 => 'リピーター管理';
@override @override
String get room_management => 'ルームサーバーの管理'; String get room_management => 'ルームサーバーの管理';
@override @override
String get repeater_guest => '繰り返し送信に関する情報'; String get repeater_guest => 'リピータに関する情報';
@override @override
String get room_guest => 'ルームサーバーに関する情報'; String get room_guest => 'ルームサーバーに関する情報';
@@ -2041,7 +2074,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_status => 'ステータス'; String get repeater_status => 'ステータス';
@override @override
String get repeater_statusSubtitle => 'リピータの状態、統計情報、および隣接するネットワークの情報を表示する'; String get repeater_statusSubtitle => 'リピータの状態、統計情報、および隣接するネットワークの情報を表示する';
@override @override
String get repeater_telemetry => 'テレメトリー'; String get repeater_telemetry => 'テレメトリー';
@@ -2053,7 +2086,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_cli => 'CLI(コマンドラインインターフェース)'; String get repeater_cli => 'CLI(コマンドラインインターフェース)';
@override @override
String get repeater_cliSubtitle => 'リピータへのコマンドを送信する'; String get repeater_cliSubtitle => 'リピータへのコマンドを送信する';
@override @override
String get repeater_neighbors => '近隣住民'; String get repeater_neighbors => '近隣住民';
@@ -2065,7 +2098,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_settings => '設定'; String get repeater_settings => '設定';
@override @override
String get repeater_settingsSubtitle => 'リピータのパラメータを設定する'; String get repeater_settingsSubtitle => 'リピータのパラメータを設定する';
@override @override
String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する'; String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する';
@@ -2080,15 +2113,6 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_routingMode => 'ルーティングモード'; String get repeater_routingMode => 'ルーティングモード';
@override
String get repeater_autoUseSavedPath => '自動 (保存されたパスを使用)';
@override
String get repeater_forceFloodMode => '強制的に洪水モードを起動';
@override
String get repeater_pathManagement => '経路管理';
@override @override
String get repeater_refresh => 'リフレッシュ'; String get repeater_refresh => 'リフレッシュ';
@@ -2163,12 +2187,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String repeater_packetTxTotal(int total, String flood, String direct) { String repeater_packetTxTotal(int total, String flood, String direct) {
return '合計: $total, 洪水: $flood, 直接: $direct'; return '合計: $total, フラッド: $flood, 直接: $direct';
} }
@override @override
String repeater_packetRxTotal(int total, String flood, String direct) { String repeater_packetRxTotal(int total, String flood, String direct) {
return '合計: $total, 洪水: $flood, 直接: $direct'; return '合計: $total, フラッド: $flood, 直接: $direct';
} }
@override @override
@@ -2182,16 +2206,16 @@ class AppLocalizationsJa extends AppLocalizations {
} }
@override @override
String get repeater_settingsTitle => 'リピータ設定'; String get repeater_settingsTitle => 'リピータ設定';
@override @override
String get repeater_basicSettings => '基本設定'; String get repeater_basicSettings => '基本設定';
@override @override
String get repeater_repeaterName => '送信装置'; String get repeater_repeaterName => 'リピータ';
@override @override
String get repeater_repeaterNameHelper => 'このリピータの名前'; String get repeater_repeaterNameHelper => 'このリピータの名前';
@override @override
String get repeater_adminPassword => '管理者パスワード'; String get repeater_adminPassword => '管理者パスワード';
@@ -2251,7 +2275,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_packetForwarding => 'パケット転送'; String get repeater_packetForwarding => 'パケット転送';
@override @override
String get repeater_packetForwardingSubtitle => 'リピータがパケットを転送できるように設定する'; String get repeater_packetForwardingSubtitle => 'リピータがパケットを転送できるように設定する';
@override @override
String get repeater_guestAccess => 'ゲストへのアクセス'; String get repeater_guestAccess => 'ゲストへのアクセス';
@@ -2277,7 +2301,7 @@ class AppLocalizationsJa extends AppLocalizations {
} }
@override @override
String get repeater_floodAdvertInterval => '洪水に関する広告の表示間隔'; String get repeater_floodAdvertInterval => 'フラッドに関する広告の表示間隔';
@override @override
String repeater_floodAdvertIntervalHours(int hours) { String repeater_floodAdvertIntervalHours(int hours) {
@@ -2291,13 +2315,13 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_dangerZone => '危険区域'; String get repeater_dangerZone => '危険区域';
@override @override
String get repeater_rebootRepeater => 'リピータを再起動する'; String get repeater_rebootRepeater => 'リピータを再起動する';
@override @override
String get repeater_rebootRepeaterSubtitle => 'リピータデバイスを再起動する'; String get repeater_rebootRepeaterSubtitle => 'リピータデバイスを再起動する';
@override @override
String get repeater_rebootRepeaterConfirm => '本当にこのリピータを再起動したいですか?'; String get repeater_rebootRepeaterConfirm => '本当にこのリピータを再起動したいですか?';
@override @override
String get repeater_regenerateIdentityKey => 'IDキーの再生成'; String get repeater_regenerateIdentityKey => 'IDキーの再生成';
@@ -2307,17 +2331,17 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_regenerateIdentityKeyConfirm => String get repeater_regenerateIdentityKeyConfirm =>
'これにより、リピータには新しい識別情報が割り当てられます。続行しますか?'; 'これにより、リピータには新しい識別情報が割り当てられます。続行しますか?';
@override @override
String get repeater_eraseFileSystem => 'ファイルシステムを削除する'; String get repeater_eraseFileSystem => 'ファイルシステムを削除する';
@override @override
String get repeater_eraseFileSystemSubtitle => 'リピータファイルシステムをフォーマットする'; String get repeater_eraseFileSystemSubtitle => 'リピータファイルシステムをフォーマットする';
@override @override
String get repeater_eraseFileSystemConfirm => String get repeater_eraseFileSystemConfirm =>
'警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!'; '警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!';
@override @override
String get repeater_eraseSerialOnly => 'Erase機能は、シリアルコンソール経由でのみ利用可能です。'; String get repeater_eraseSerialOnly => 'Erase機能は、シリアルコンソール経由でのみ利用可能です。';
@@ -2363,7 +2387,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_loopDetect => 'ループ検出'; String get repeater_loopDetect => 'ループ検出';
@override @override
String get repeater_loopDetectHelper => 'ルーティングループに見えるような、洪水パケットを送信する'; String get repeater_loopDetectHelper => 'ルーティングループを検知する';
@override @override
String get repeater_loopDetectOff => 'オフ'; String get repeater_loopDetectOff => 'オフ';
@@ -2392,7 +2416,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_ownerInfo => '事業者の情報'; String get repeater_ownerInfo => '事業者の情報';
@override @override
String get repeater_ownerInfoHelper => 'このリピータに関する公開メタデータ'; String get repeater_ownerInfoHelper => 'このリピータに関する公開メタデータ';
@override @override
String get repeater_refreshOwnerInfo => 'オペレーター情報の更新'; String get repeater_refreshOwnerInfo => 'オペレーター情報の更新';
@@ -2401,7 +2425,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_floodMax => '最大ホップ数'; String get repeater_floodMax => '最大ホップ数';
@override @override
String get repeater_floodMaxHelper => '洪水パケットが移動できる最大ホップ数 (0-64)'; String get repeater_floodMaxHelper => 'フラッドパケットが移動できる最大ホップ数 (0-64)';
@override @override
String get repeater_advancedSettings => '高度な'; String get repeater_advancedSettings => '高度な';
@@ -2414,14 +2438,14 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'フロイド・TXでの遅延'; String get repeater_txDelay => 'フロイド・TXでの遅延';
@override @override
String get repeater_txDelayHelper => String get repeater_txDelayHelper =>
'洪水時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。'; 'フラッド時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。';
@override @override
String get repeater_directTxDelay => '直接的なTX遅延'; String get repeater_directTxDelay => '直接的なTX遅延';
@@ -2448,16 +2472,16 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_actionsTitle => '行動'; String get repeater_actionsTitle => '行動';
@override @override
String get repeater_sendAdvert => '洪水に関する広告を送信'; String get repeater_sendAdvert => 'フラッドに関する広告を送信';
@override @override
String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、洪水に関する広告を放送する'; String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、フラッドに関する広告を放送する';
@override @override
String get repeater_sendAdvertZeroHop => 'ゼロホップ形式の広告を送信する'; String get repeater_sendAdvertZeroHop => 'ゼロホップ形式の広告を送信する';
@override @override
String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(中継なし)'; String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(リピータなし)';
@override @override
String get repeater_clockSync => '現在、時刻を同期する'; String get repeater_clockSync => '現在、時刻を同期する';
@@ -2477,7 +2501,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_settingsSavedRebootNeeded => String get repeater_settingsSavedRebootNeeded =>
'設定を保存しました — リピータを再起動して適用してください'; '設定を保存しました — リピータを再起動して適用してください';
@override @override
String repeater_settingsPartialFailure(String failures) { String repeater_settingsPartialFailure(String failures) {
@@ -2518,7 +2542,7 @@ class AppLocalizationsJa extends AppLocalizations {
} }
@override @override
String get repeater_cliTitle => 'リピータのコマンドラインインターフェース'; String get repeater_cliTitle => 'リピータのコマンドラインインターフェース';
@override @override
String get repeater_debugNextCommand => '次のコマンドのデバッグ'; String get repeater_debugNextCommand => '次のコマンドのデバッグ';
@@ -2609,7 +2633,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_cliHelpSetTx => 'LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)'; String get repeater_cliHelpSetTx => 'LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)';
@override @override
String get repeater_cliHelpSetRepeat => 'このノードに対するリピータの役割を有効化または無効化します。'; String get repeater_cliHelpSetRepeat => 'このノードに対するリピータの役割を有効化または無効化します。';
@override @override
String get repeater_cliHelpSetAllowReadOnly => String get repeater_cliHelpSetAllowReadOnly =>
@@ -2617,7 +2641,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliHelpSetFloodMax => String get repeater_cliHelpSetFloodMax =>
'インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。'; 'インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。';
@override @override
String get repeater_cliHelpSetIntThresh => String get repeater_cliHelpSetIntThresh =>
@@ -2636,7 +2660,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliHelpSetFloodAdvertInterval => String get repeater_cliHelpSetFloodAdvertInterval =>
'洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。'; 'フラッド広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。';
@override @override
String get repeater_cliHelpSetGuestPassword => String get repeater_cliHelpSetGuestPassword =>
@@ -2710,7 +2734,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliHelpNeighbors => String get repeater_cliHelpNeighbors =>
'ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。'; 'ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。';
@override @override
String get repeater_cliHelpNeighborRemove => String get repeater_cliHelpNeighborRemove =>
@@ -2718,7 +2742,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliHelpRegion => String get repeater_cliHelpRegion =>
'(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。'; '(特定のシリーズのみ)定義されたすべての地域と、現在のフラッド許可状況を一覧表示します。';
@override @override
String get repeater_cliHelpRegionLoad => String get repeater_cliHelpRegionLoad =>
@@ -2737,7 +2761,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliHelpRegionAllowf => String get repeater_cliHelpRegionAllowf =>
'指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)'; '指定された領域に対して、「フラッド」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)';
@override @override
String get repeater_cliHelpRegionDenyf => String get repeater_cliHelpRegionDenyf =>
@@ -2793,7 +2817,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_logging => 'ログ記録'; String get repeater_logging => 'ログ記録';
@override @override
String get repeater_neighborsRepeaterOnly => '近隣住民(リピータのみ)'; String get repeater_neighborsRepeaterOnly => '近隣住民(リピータのみ)';
@override @override
String get repeater_regionManagementRepeaterOnly => '地域管理(ブロードキャスト用のみ)'; String get repeater_regionManagementRepeaterOnly => '地域管理(ブロードキャスト用のみ)';
@@ -2838,7 +2862,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliHelpDiscoverNeighbors => String get repeater_cliHelpDiscoverNeighbors =>
'近隣のノードに対して、ノードの探索リクエストを送信します。(中継機能のみ)'; '近隣のノードに対して、ノードの探索リクエストを送信します。(リピータ機能のみ)';
@override @override
String get repeater_cliHelpPowersaving => '省電力モードがオンになっているかどうかを表示します。'; String get repeater_cliHelpPowersaving => '省電力モードがオンになっているかどうかを表示します。';
@@ -2887,7 +2911,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_cliHelpGetName => '設定されたノードの名前を表示します。'; String get repeater_cliHelpGetName => '設定されたノードの名前を表示します。';
@override @override
String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。'; String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。';
@override @override
String get repeater_cliHelpGetPublicKey => 'デバイスの公開鍵を表示します。'; String get repeater_cliHelpGetPublicKey => 'デバイスの公開鍵を表示します。';
@@ -2897,7 +2921,7 @@ class AppLocalizationsJa extends AppLocalizations {
'(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。'; '(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。';
@override @override
String get repeater_cliHelpGetRepeat => 'パケット転送(リピータ機能)が有効になっているかどうかを表示します。'; String get repeater_cliHelpGetRepeat => 'パケット転送(リピータ機能)が有効になっているかどうかを表示します。';
@override @override
String get repeater_cliHelpGetTx => '現在のTX(送信)電力のdBm値を表示します。'; String get repeater_cliHelpGetTx => '現在のTX(送信)電力のdBm値を表示します。';
@@ -2936,7 +2960,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliHelpGetFloodAdvertInterval => String get repeater_cliHelpGetFloodAdvertInterval =>
'洪水に関する広告の放送時間を時間単位で表示します。'; 'フラッドに関する広告の放送時間を時間単位で表示します。';
@override @override
String get repeater_cliHelpGetGuestPassword => '設定されたゲストパスワードを表示します。'; String get repeater_cliHelpGetGuestPassword => '設定されたゲストパスワードを表示します。';
@@ -2951,13 +2975,13 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_cliHelpGetRxDelay => 'rxdelay の基本値を表示します。'; String get repeater_cliHelpGetRxDelay => 'rxdelay の基本値を表示します。';
@override @override
String get repeater_cliHelpGetTxDelay => '洪水モードにおける送信遅延の要因を示します。'; String get repeater_cliHelpGetTxDelay => 'フラッドモードにおける送信遅延の要因を示します。';
@override @override
String get repeater_cliHelpGetDirectTxDelay => 'ダイレクトモードの遅延要素を示します。'; String get repeater_cliHelpGetDirectTxDelay => 'ダイレクトモードの遅延要素を示します。';
@override @override
String get repeater_cliHelpGetFloodMax => '最大浸水範囲の回数を表示します。'; String get repeater_cliHelpGetFloodMax => 'フラッドパケットの最大ホップ数を表示します。';
@override @override
String get repeater_cliHelpGetOwnerInfo => '所有者の連絡先情報を表示します。'; String get repeater_cliHelpGetOwnerInfo => '所有者の連絡先情報を表示します。';
@@ -2969,7 +2993,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_cliHelpGetLoopDetect => 'ループ検出の感度を示す。'; String get repeater_cliHelpGetLoopDetect => 'ループ検出の感度を示す。';
@override @override
String get repeater_cliHelpGetAcl => '(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。'; String get repeater_cliHelpGetAcl => '(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。';
@override @override
String get repeater_cliHelpGetBridgeEnabled => '橋が有効になっているかどうかを表示します。'; String get repeater_cliHelpGetBridgeEnabled => '橋が有効になっているかどうかを表示します。';
@@ -3032,10 +3056,10 @@ class AppLocalizationsJa extends AppLocalizations {
'デフォルトの地域範囲を設定します。「<null>」を使用すると、設定をリセットできます。'; 'デフォルトの地域範囲を設定します。「<null>」を使用すると、設定をリセットできます。';
@override @override
String get repeater_cliHelpRegionListAllowed => '洪水時の通行が許可されている地域の一覧'; String get repeater_cliHelpRegionListAllowed => 'フラッド時の通行が許可されている地域の一覧';
@override @override
String get repeater_cliHelpRegionListDenied => '洪水による交通を遮断している地域の一覧'; String get repeater_cliHelpRegionListDenied => 'フラッドによる交通を遮断している地域の一覧';
@override @override
String get repeater_cliHelpStatsPackets => '(シリアルのみ)パケットレベルの統計情報を表示します。'; String get repeater_cliHelpStatsPackets => '(シリアルのみ)パケットレベルの統計情報を表示します。';
@@ -3245,7 +3269,7 @@ class AppLocalizationsJa extends AppLocalizations {
} }
@override @override
String get neighbors_repeatersNeighbors => '繰り返し送信する、近隣'; String get neighbors_repeatersNeighbors => '近隣のリピータ';
@override @override
String get neighbors_noData => '近隣のデータは利用できません。'; String get neighbors_noData => '近隣のデータは利用できません。';
@@ -3270,7 +3294,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get channelPath_otherObservedPaths => '観察されたその他の経路'; String get channelPath_otherObservedPaths => '観察されたその他の経路';
@override @override
String get channelPath_repeaterHops => 'ホップの繰り返し'; String get channelPath_repeaterHops => 'リピータホップ';
@override @override
String get channelPath_noHopDetails => 'このパッケージに関する詳細な情報は提供されていません。'; String get channelPath_noHopDetails => 'このパッケージに関する詳細な情報は提供されていません。';
@@ -3285,7 +3309,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get channelPath_timeLabel => '時間'; String get channelPath_timeLabel => '時間';
@override @override
String get channelPath_repeatsLabel => '繰り返し'; String get channelPath_repeatsLabel => 'リピータ';
@override @override
String channelPath_pathLabel(int index) { String channelPath_pathLabel(int index) {
@@ -3317,7 +3341,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get channelPath_unknownPath => '不明'; String get channelPath_unknownPath => '不明';
@override @override
String get channelPath_floodPath => '洪水'; String get channelPath_floodPath => 'フラッド';
@override @override
String get channelPath_directPath => '直接'; String get channelPath_directPath => '直接';
@@ -3336,7 +3360,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get channelPath_mapTitle => '経路図'; String get channelPath_mapTitle => '経路図';
@override @override
String get channelPath_noRepeaterLocations => 'この経路には、中継装置の設置場所がありません。'; String get channelPath_noRepeaterLocations => 'この経路にリピータの位置情報はありません。';
@override @override
String channelPath_primaryPath(int index) { String channelPath_primaryPath(int index) {
@@ -3556,7 +3580,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get listFilter_users => '利用者'; String get listFilter_users => '利用者';
@override @override
String get listFilter_repeaters => '繰り返し送信装置'; String get listFilter_repeaters => 'リピータ';
@override @override
String get listFilter_roomServers => 'ルーム用サーバー'; String get listFilter_roomServers => 'ルーム用サーバー';
@@ -3756,10 +3780,10 @@ class AppLocalizationsJa extends AppLocalizations {
String get contacts_ping => 'パング'; String get contacts_ping => 'パング';
@override @override
String get contacts_repeaterPathTrace => 'リピータまでの経路を追跡する'; String get contacts_repeaterPathTrace => 'リピータまでの経路を追跡する';
@override @override
String get contacts_repeaterPing => 'PING 繰り返し'; String get contacts_repeaterPing => 'リピータにPING';
@override @override
String get contacts_roomPathTrace => '部屋のサーバーへの経路を追跡する'; String get contacts_roomPathTrace => '部屋のサーバーへの経路を追跡する';
@@ -3791,7 +3815,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get contacts_zeroHopAdvert => 'ゼロホップ広告'; String get contacts_zeroHopAdvert => 'ゼロホップ広告';
@override @override
String get contacts_floodAdvert => '洪水に関する広告'; String get contacts_floodAdvert => 'フラッドに関する広告';
@override @override
String get contacts_copyAdvertToClipboard => '広告をクリップボードにコピー'; String get contacts_copyAdvertToClipboard => '広告をクリップボードにコピー';
@@ -3862,7 +3886,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get notification_receivedNewMessage => '新しいメッセージを受信'; String get notification_receivedNewMessage => '新しいメッセージを受信';
@override @override
String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピータ/ルームサーバー'; String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピータ/ルームサーバー';
@override @override
String get settings_gpxExportRepeatersSubtitle => String get settings_gpxExportRepeatersSubtitle =>
@@ -3895,7 +3919,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get settings_gpxExportError => 'エクスポート時にエラーが発生しました。'; String get settings_gpxExportError => 'エクスポート時にエラーが発生しました。';
@override @override
String get settings_gpxExportRepeatersRoom => '中継装置およびルームサーバーの設置場所'; String get settings_gpxExportRepeatersRoom => 'リピータ/ルームサーバーの位置情報';
@override @override
String get settings_gpxExportChat => '関連施設'; String get settings_gpxExportChat => '関連施設';
@@ -3911,7 +3935,7 @@ class AppLocalizationsJa extends AppLocalizations {
'meshcore-open GPX形式の地図データのエクスポート'; 'meshcore-open GPX形式の地図データのエクスポート';
@override @override
String get snrIndicator_nearByRepeaters => '近くの電波中継局'; String get snrIndicator_nearByRepeaters => '近くのリピータ';
@override @override
String get snrIndicator_lastSeen => '最後に確認された場所'; String get snrIndicator_lastSeen => '最後に確認された場所';
@@ -3933,11 +3957,11 @@ class AppLocalizationsJa extends AppLocalizations {
'利用者が自動的に発見したユーザーを追加できるようにする。'; '利用者が自動的に発見したユーザーを追加できるようにする。';
@override @override
String get contactsSettings_autoAddRepeatersTitle => '自動で繰り返し設定'; String get contactsSettings_autoAddRepeatersTitle => 'リピータを自動追加';
@override @override
String get contactsSettings_autoAddRepeatersSubtitle => String get contactsSettings_autoAddRepeatersSubtitle =>
'発見した中継局を、自動的に追加できるようにする。'; '発見したリピータを、自動的に追加できるようにする。';
@override @override
String get contactsSettings_autoAddRoomServersTitle => '自動でルームサーバーを追加'; String get contactsSettings_autoAddRoomServersTitle => '自動でルームサーバーを追加';
@@ -4212,4 +4236,28 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -132
View File
@@ -92,6 +92,24 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get common_disable => '비활성화'; 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 @override
String get common_reboot => '재부팅'; String get common_reboot => '재부팅';
@@ -749,11 +767,6 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get appSettings_maxMessageRetriesSubtitle => '메시지를 실패로 처리하기 전 시도 횟수'; String get appSettings_maxMessageRetriesSubtitle => '메시지를 실패로 처리하기 전 시도 횟수';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => '배터리'; String get appSettings_battery => '배터리';
@@ -945,6 +958,15 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get contacts_newGroup => '새로운 그룹'; 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 @override
String get contacts_groupName => '그룹 이름'; String get contacts_groupName => '그룹 이름';
@@ -1411,34 +1433,6 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get debugFrame_hexDump => '헥스 덤프:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1450,61 +1444,149 @@ class AppLocalizationsKo extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => '성공 사례';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => '경로 제거'; String get chat_removePath => '경로 제거';
@override @override
String get chat_noPathHistoryYet => '아직 경로 기록이 없습니다.\n경로를 찾기 위해 메시지를 보내세요.'; 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 @override
String get chat_pathCleared => '경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.'; String get chat_pathCleared => '경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.';
@override
String get chat_floodModeSubtitle => '앱 바에서 라우팅 스위치를 사용';
@override
String get chat_floodModeEnabled =>
'홍수 모드 활성화됨. 앱 바의 경로 아이콘을 사용하여 다시 전환할 수 있습니다.';
@override @override
String get chat_fullPath => '전체 경로'; String get chat_fullPath => '전체 경로';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'경로 정보는 아직 제공되지 않습니다. 메시지를 보내어 다시 시도해 보세요.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Path set: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결'; String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결';
@@ -1958,64 +2040,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get common_clear => '명확하게'; 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 @override
String get path_currentPathLabel => '현재 경로'; 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 @override
String get path_noRepeatersFound => '반복 장치 또는 서버는 찾을 수 없습니다.'; 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 @override
String get repeater_management => '리피터 관리'; String get repeater_management => '리피터 관리';
@@ -2077,15 +2107,6 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get repeater_routingMode => '라우팅 방식'; String get repeater_routingMode => '라우팅 방식';
@override
String get repeater_autoUseSavedPath => '자동 (저장된 경로 사용)';
@override
String get repeater_forceFloodMode => '강수 모드 활성화';
@override
String get repeater_pathManagement => '경로 관리';
@override @override
String get repeater_refresh => '새롭게'; String get repeater_refresh => '새롭게';
@@ -4213,4 +4234,28 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get common_disable => 'Uitschakelen'; 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 @override
String get common_reboot => 'Herstarten'; String get common_reboot => 'Herstarten';
@@ -786,11 +804,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd'; '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 @override
String get appSettings_battery => 'Batterij'; String get appSettings_battery => 'Batterij';
@@ -991,6 +1004,15 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Nieuwe Groep'; 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 @override
String get contacts_groupName => 'Groepnaam'; String get contacts_groupName => 'Groepnaam';
@@ -1468,34 +1490,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Hex-dump:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1507,12 +1501,6 @@ class AppLocalizationsNl extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Succesvol';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Pad verwijderen'; String get chat_removePath => 'Pad verwijderen';
@@ -1520,52 +1508,144 @@ class AppLocalizationsNl extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Geen geschiedenis van paden nog beschikbaar.\nVerzend een bericht om paden te ontdekken.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Pad is vrijgegeven. Volgende bericht herontdekt route.'; '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 @override
String get chat_fullPath => 'Volledige Pad'; String get chat_fullPath => 'Volledige Pad';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'De paddetails zijn nog niet beschikbaar. Probeer een bericht te sturen om te vernieuwen.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Pad ingesteld: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Opgeslagen lokaal. Verbinden om te synchroniseren.'; 'Opgeslagen lokaal. Verbinden om te synchroniseren.';
@@ -2034,65 +2114,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get common_clear => 'Schoonmaken'; 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 @override
String get path_currentPathLabel => 'Huidige pad'; 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 @override
String get path_noRepeatersFound => 'Geen repeaters of roomservers gevonden.'; 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 @override
String get repeater_management => 'Beheer Repeaters'; String get repeater_management => 'Beheer Repeaters';
@@ -2157,16 +2184,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Routeerwijze'; 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 @override
String get repeater_refresh => 'Vernieuwen'; String get repeater_refresh => 'Vernieuwen';
@@ -4422,4 +4439,28 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -139
View File
@@ -92,6 +92,24 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get common_disable => 'Wyłącz'; 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 @override
String get common_reboot => 'Uruchom ponownie'; String get common_reboot => 'Uruchom ponownie';
@@ -796,11 +814,6 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej'; '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 @override
String get appSettings_battery => 'Bateria'; String get appSettings_battery => 'Bateria';
@@ -1009,6 +1022,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Nowa Grupa'; 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 @override
String get contacts_groupName => 'Nazwa grupy'; String get contacts_groupName => 'Nazwa grupy';
@@ -1491,35 +1513,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Zrzut hex:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1533,12 +1526,6 @@ class AppLocalizationsPl extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Sukcesy';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Usuń ścieżkę'; String get chat_removePath => 'Usuń ścieżkę';
@@ -1546,52 +1533,144 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Brak historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Ścieżka wyczyszczona. Następna wiadomość odnajdzie trasę.'; 'Ś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 @override
String get chat_fullPath => 'Pełna ścieżka'; String get chat_fullPath => 'Pełna ścieżka';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Ścieżka ustawiona: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Zapisano lokalnie. Połącz się, aby zsynchronizować.'; 'Zapisano lokalnie. Połącz się, aby zsynchronizować.';
@@ -2061,68 +2140,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get common_clear => 'Wyczyść'; 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 @override
String get path_currentPathLabel => 'Aktualna ścieżka'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Nie znaleziono przekaźników ani serwerów pokoi.'; '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 @override
String get repeater_management => 'Zarządzanie przekaźnikami'; String get repeater_management => 'Zarządzanie przekaźnikami';
@@ -2187,16 +2211,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Tryb routingu'; 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 @override
String get repeater_refresh => 'Odśwież'; String get repeater_refresh => 'Odśwież';
@@ -4459,4 +4473,28 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -136
View File
@@ -92,6 +92,24 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get common_disable => 'Desativar'; 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 @override
String get common_reboot => 'Reiniciar'; String get common_reboot => 'Reiniciar';
@@ -793,11 +811,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Número de tentativas de reenvio antes de classificar uma mensagem como falha.'; '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 @override
String get appSettings_battery => 'Bateria'; String get appSettings_battery => 'Bateria';
@@ -999,6 +1012,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Novo Grupo'; 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 @override
String get contacts_groupName => 'Nome do grupo'; String get contacts_groupName => 'Nome do grupo';
@@ -1478,34 +1500,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Espaço Hexadecimal:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1517,12 +1511,6 @@ class AppLocalizationsPt extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Sucessos';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Remover caminho'; String get chat_removePath => 'Remover caminho';
@@ -1530,53 +1518,144 @@ class AppLocalizationsPt extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Caminho limpo. A próxima mensagem redescobrirá a rota.'; '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 @override
String get chat_fullPath => 'Caminho Completo'; String get chat_fullPath => 'Caminho Completo';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Caminho definido: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Salvo localmente. Conectar para sincronizar.'; 'Salvo localmente. Conectar para sincronizar.';
@@ -2044,66 +2123,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get common_clear => 'Limpar'; 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 @override
String get path_currentPathLabel => 'Caminho atual'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Não foram encontrados repetidores ou servidores de sala.'; '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 @override
String get repeater_management => 'Gerenciamento de Repetidor'; String get repeater_management => 'Gerenciamento de Repetidor';
@@ -2168,15 +2194,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Modo de roteamento'; 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 @override
String get repeater_refresh => 'Atualizar'; String get repeater_refresh => 'Atualizar';
@@ -4435,4 +4452,28 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -140
View File
@@ -92,6 +92,24 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get common_disable => 'Выключить'; 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 @override
String get common_reboot => 'Перезагрузить'; String get common_reboot => 'Перезагрузить';
@@ -795,11 +813,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.'; 'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => 'Батарея'; String get appSettings_battery => 'Батарея';
@@ -1000,6 +1013,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Новая группа'; 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 @override
String get contacts_groupName => 'Имя группы'; String get contacts_groupName => 'Имя группы';
@@ -1479,35 +1501,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1521,12 +1514,6 @@ class AppLocalizationsRu extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'успешно';
@override
String get chat_score => 'Оценка';
@override @override
String get chat_removePath => 'Удалить маршрут'; String get chat_removePath => 'Удалить маршрут';
@@ -1534,54 +1521,144 @@ class AppLocalizationsRu extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; 'История маршрутов пока пуста.\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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Маршрут очищен. Следующее сообщение обновит маршрут.'; 'Маршрут очищен. Следующее сообщение обновит маршрут.';
@override
String get chat_floodModeSubtitle =>
'Используйте переключатель маршрутизации в панели приложения';
@override
String get chat_floodModeEnabled =>
'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.';
@override @override
String get chat_fullPath => 'Полный маршрут'; String get chat_fullPath => 'Полный маршрут';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'хопов',
many: 'хопов', @override
few: 'хопа', String get routing_modeManual => 'Manual';
one: 'хоп',
); @override
return 'Маршрут установлен: $hopCount $_temp0$status'; 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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Сохранено локально. Подключитесь для синхронизации.'; 'Сохранено локально. Подключитесь для синхронизации.';
@@ -2049,66 +2126,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get common_clear => 'Очистить'; 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 @override
String get path_currentPathLabel => 'Текущий маршрут'; 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 @override
String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; 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 @override
String get repeater_management => 'Управление репитером'; String get repeater_management => 'Управление репитером';
@@ -2173,16 +2196,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Режим маршрутизации'; String get repeater_routingMode => 'Режим маршрутизации';
@override
String get repeater_autoUseSavedPath =>
'Авто (использовать сохранённый маршрут)';
@override
String get repeater_forceFloodMode => 'Принудительный режим рассылки';
@override
String get repeater_pathManagement => 'Управление маршрутами';
@override @override
String get repeater_refresh => 'Обновить'; String get repeater_refresh => 'Обновить';
@@ -4453,4 +4466,28 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Неизвестно'; 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';
} }
+177 -137
View File
@@ -92,6 +92,24 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get common_disable => 'Zakázať'; 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 @override
String get common_reboot => 'Restartovať'; String get common_reboot => 'Restartovať';
@@ -782,11 +800,6 @@ class AppLocalizationsSk extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Počet pokusov o odošleť pred označením správy ako neúspešnej'; '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 @override
String get appSettings_battery => 'Batéria'; String get appSettings_battery => 'Batéria';
@@ -988,6 +1001,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Nová skupina'; 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 @override
String get contacts_groupName => 'Názov skupiny'; String get contacts_groupName => 'Názov skupiny';
@@ -1469,35 +1491,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Hexová analýza:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1509,12 +1502,6 @@ class AppLocalizationsSk extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Úspechy';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Odstrániť cestu'; String get chat_removePath => 'Odstrániť cestu';
@@ -1522,52 +1509,144 @@ class AppLocalizationsSk extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.'; '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 @override
String get chat_fullPath => 'Celá cesta'; String get chat_fullPath => 'Celá cesta';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Cesta nastavená: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Uložené lokálne. Spojte sa na synchronizáciu.'; 'Uložené lokálne. Spojte sa na synchronizáciu.';
@@ -2035,66 +2114,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get common_clear => 'Zmazať'; 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 @override
String get path_currentPathLabel => 'Aktuálny priebeh'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Nenašli sa žiadne opakovače ani serverové miestnosti.'; '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 @override
String get repeater_management => 'Správa opakérov'; String get repeater_management => 'Správa opakérov';
@@ -2159,16 +2185,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Režim trasy'; 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 @override
String get repeater_refresh => 'Obnoviť'; String get repeater_refresh => 'Obnoviť';
@@ -4418,4 +4434,28 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -133
View File
@@ -92,6 +92,24 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get common_disable => 'Izklopiti'; 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 @override
String get common_reboot => 'Ponoviti'; String get common_reboot => 'Ponoviti';
@@ -783,11 +801,6 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno'; 'Š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 @override
String get appSettings_battery => 'Baterija'; String get appSettings_battery => 'Baterija';
@@ -987,6 +1000,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Nova skupina'; 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 @override
String get contacts_groupName => 'Ime skupine'; String get contacts_groupName => 'Ime skupine';
@@ -1466,34 +1488,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Izpis heksadecimalnih vrednosti:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1505,12 +1499,6 @@ class AppLocalizationsSl extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Uspešni';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Izbriši pot'; String get chat_removePath => 'Izbriši pot';
@@ -1518,51 +1506,144 @@ class AppLocalizationsSl extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.'; '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 @override
String get chat_fullPath => 'Polna pot'; String get chat_fullPath => 'Polna pot';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hops',
one: 'hop', @override
); String get routing_modeManual => 'Manual';
return 'Pot nastavljen: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Shrano lokalno. Povežite se za sinhronizacijo.'; 'Shrano lokalno. Povežite se za sinhronizacijo.';
@@ -2032,65 +2113,13 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get common_clear => 'Ponoviti'; 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 @override
String get path_currentPathLabel => 'Trenutna pot'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Ne najdenih ponoviteljev ali strežnikov sob.'; '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 @override
String get repeater_management => 'Upravljanje ponovitve'; String get repeater_management => 'Upravljanje ponovitve';
@@ -2156,15 +2185,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Navodilo za usmerjevalni način'; 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 @override
String get repeater_refresh => 'Ponovno obnavljati'; String get repeater_refresh => 'Ponovno obnavljati';
@@ -4416,4 +4436,28 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -133
View File
@@ -92,6 +92,24 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get common_disable => 'Inaktivera'; 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 @override
String get common_reboot => 'Start om'; String get common_reboot => 'Start om';
@@ -777,11 +795,6 @@ class AppLocalizationsSv extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.'; '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 @override
String get appSettings_battery => 'Batteri'; String get appSettings_battery => 'Batteri';
@@ -982,6 +995,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Ny grupp'; 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 @override
String get contacts_groupName => 'Gruppnamn'; String get contacts_groupName => 'Gruppnamn';
@@ -1460,35 +1482,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Hexdump:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1500,12 +1493,6 @@ class AppLocalizationsSv extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'framgångar';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => 'Ta bort sökväg'; String get chat_removePath => 'Ta bort sökväg';
@@ -1513,50 +1500,144 @@ class AppLocalizationsSv extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.'; '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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.'; '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 @override
String get chat_fullPath => 'Fullständig sökväg'; String get chat_fullPath => 'Fullständig sökväg';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'hoppar',
one: 'hopp', @override
); String get routing_modeManual => 'Manual';
return 'Sökväg inställd: $hopCount $_temp0 - $status';
@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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Sparat lokalt. Anslut för att synkronisera.'; 'Sparat lokalt. Anslut för att synkronisera.';
@@ -2021,65 +2102,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get common_clear => 'Rensa'; 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 @override
String get path_currentPathLabel => 'Nuvarande sökväg'; 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 @override
String get path_noRepeatersFound => String get path_noRepeatersFound =>
'Inga återuppspelare eller rumsservrar hittades.'; '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 @override
String get repeater_management => 'Återuppspelarens Hantering'; String get repeater_management => 'Återuppspelarens Hantering';
@@ -2144,15 +2173,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Ruttläge'; 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 @override
String get repeater_refresh => 'Uppdatera'; String get repeater_refresh => 'Uppdatera';
@@ -4391,4 +4411,28 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+177 -140
View File
@@ -92,6 +92,24 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get common_disable => 'Вимкнути'; 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 @override
String get common_reboot => 'Перезавантажити'; String get common_reboot => 'Перезавантажити';
@@ -789,11 +807,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get appSettings_maxMessageRetriesSubtitle => String get appSettings_maxMessageRetriesSubtitle =>
'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале'; 'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => 'Батарея'; String get appSettings_battery => 'Батарея';
@@ -995,6 +1008,15 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get contacts_newGroup => 'Нова група'; 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 @override
String get contacts_groupName => 'Назва групи'; String get contacts_groupName => 'Назва групи';
@@ -1474,35 +1496,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get debugFrame_hexDump => 'Дамп Hex:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@@ -1516,12 +1509,6 @@ class AppLocalizationsUk extends AppLocalizations {
return '$count $_temp0'; return '$count $_temp0';
} }
@override
String get chat_successes => 'Успішно';
@override
String get chat_score => 'Оцінка';
@override @override
String get chat_removePath => 'Видалити шлях'; String get chat_removePath => 'Видалити шлях';
@@ -1529,54 +1516,144 @@ class AppLocalizationsUk extends AppLocalizations {
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.'; 'Історія шляхів недоступна.\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 @override
String get chat_pathCleared => String get chat_pathCleared =>
'Шлях очищено. Наступне повідомлення оновить маршрут.'; 'Шлях очищено. Наступне повідомлення оновить маршрут.';
@override
String get chat_floodModeSubtitle =>
'Використовувати перемикач маршрутизації в панелі застосунку';
@override
String get chat_floodModeEnabled =>
'Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.';
@override @override
String get chat_fullPath => 'Повний шлях'; String get chat_fullPath => 'Повний шлях';
@override @override
String get chat_pathDetailsNotAvailable => String get routing_title => 'Routing';
'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
String _temp0 = intl.Intl.pluralLogic(
hopCount, @override
locale: localeName, String get routing_modeFlood => 'Flood';
other: 'переходів',
many: 'переходів', @override
few: 'переходи', String get routing_modeManual => 'Manual';
one: 'перехід',
); @override
return 'Шлях встановлено: $hopCount $_temp0 - $status'; 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 @override
String get chat_pathSavedLocally => String get chat_pathSavedLocally =>
'Збережено локально. Підключіться для синхронізації.'; 'Збережено локально. Підключіться для синхронізації.';
@@ -2043,67 +2120,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get common_clear => 'Очистити'; 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 @override
String get path_currentPathLabel => 'Поточний шлях'; 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 @override
String get path_noRepeatersFound => 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 @override
String get repeater_management => 'Керування ретранслятором'; String get repeater_management => 'Керування ретранслятором';
@@ -2168,16 +2191,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get repeater_routingMode => 'Режим маршрутизації'; String get repeater_routingMode => 'Режим маршрутизації';
@override
String get repeater_autoUseSavedPath =>
'Авто (використовувати збережений шлях)';
@override
String get repeater_forceFloodMode => 'Примусово через всю мережу';
@override
String get repeater_pathManagement => 'Керування шляхами';
@override @override
String get repeater_refresh => 'Оновити'; String get repeater_refresh => 'Оновити';
@@ -4454,4 +4467,28 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Невідомо'; 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';
} }
+177 -113
View File
@@ -92,6 +92,24 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get common_disable => '禁用'; 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 @override
String get common_reboot => '重启'; String get common_reboot => '重启';
@@ -735,11 +753,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数'; String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override @override
String get appSettings_battery => '电池'; String get appSettings_battery => '电池';
@@ -931,6 +944,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get contacts_newGroup => '新建群聊'; 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 @override
String get contacts_groupName => '群聊名称'; String get contacts_groupName => '群聊名称';
@@ -1397,85 +1419,154 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get debugFrame_hexDump => '十六进制数据:'; 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 @override
String chat_hopsCount(int count) { String chat_hopsCount(int count) {
return '$count'; return '$count';
} }
@override
String get chat_successes => '成功';
@override
String get chat_score => 'Score';
@override @override
String get chat_removePath => '移除路径'; String get chat_removePath => '移除路径';
@override @override
String get chat_noPathHistoryYet => '暂无路径历史。\n发送消息以探索路径。'; 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 @override
String get chat_pathCleared => '路径已清除。下一条消息将重新路由。'; String get chat_pathCleared => '路径已清除。下一条消息将重新路由。';
@override
String get chat_floodModeSubtitle => '在应用栏中切换路由模式。';
@override
String get chat_floodModeEnabled => '泛洪模式已启用。可通过应用栏的路由图标切换。';
@override @override
String get chat_fullPath => '完整路径'; String get chat_fullPath => '完整路径';
@override @override
String get chat_pathDetailsNotAvailable => '路径信息暂不可用,请尝试发送消息刷新。'; String get routing_title => 'Routing';
@override @override
String chat_pathSetHops(int hopCount, String status) { String get routing_modeAuto => 'Auto';
return '路径设置:$hopCount 跳 - $status';
@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 @override
String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。'; String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。';
@@ -1928,54 +2019,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get common_clear => '清除'; 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 @override
String get path_currentPathLabel => '当前路径'; 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 @override
String get path_noRepeatersFound => '未找到任何转发节点或房间服务器。'; 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 @override
String get repeater_management => '转发节点管理'; String get repeater_management => '转发节点管理';
@@ -2036,15 +2085,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get repeater_routingMode => '路由模式'; String get repeater_routingMode => '路由模式';
@override
String get repeater_autoUseSavedPath => '自动(使用保存的路径)';
@override
String get repeater_forceFloodMode => '强制泛洪模式';
@override
String get repeater_pathManagement => '路径管理';
@override @override
String get repeater_refresh => '刷新'; String get repeater_refresh => '刷新';
@@ -4086,4 +4126,28 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get contact_typeUnknown => 'Unknown'; 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';
} }
+12 -17
View File
@@ -24,11 +24,21 @@ import 'services/translation_service.dart';
import 'services/ui_view_state_service.dart'; import 'services/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart'; import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart'; import 'storage/prefs_manager.dart';
import 'theme/mesh_theme.dart';
import 'utils/app_logger.dart'; import 'utils/app_logger.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); 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 // Initialize SharedPreferences cache
await PrefsManager.initialize(); await PrefsManager.initialize();
@@ -191,23 +201,8 @@ class MeshCoreApp extends StatelessWidget {
locale: _localeFromSetting( locale: _localeFromSetting(
settingsService.settings.languageOverride, settingsService.settings.languageOverride,
), ),
theme: ThemeData( theme: MeshTheme.light(),
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), darkTheme: MeshTheme.dark(),
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,
),
),
themeMode: _themeModeFromSetting( themeMode: _themeModeFromSetting(
settingsService.settings.themeMode, settingsService.settings.themeMode,
), ),
+11 -10
View File
@@ -63,7 +63,7 @@ class AppDebugLogScreen extends StatelessWidget {
final entry = entries[index]; final entry = entries[index];
return ListTile( return ListTile(
dense: true, dense: true,
leading: _buildLevelIcon(entry.level), leading: _buildLevelIcon(context, entry.level),
title: Text( title: Text(
'[${entry.tag}] ${entry.message}', '[${entry.tag}] ${entry.message}',
style: const TextStyle( style: const TextStyle(
@@ -75,7 +75,7 @@ class AppDebugLogScreen extends StatelessWidget {
entry.formattedTime, entry.formattedTime,
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
); );
@@ -88,14 +88,14 @@ class AppDebugLogScreen extends StatelessWidget {
Icon( Icon(
Icons.bug_report_outlined, Icons.bug_report_outlined,
size: 64, size: 64,
color: Colors.grey[400], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
context.l10n.debugLog_noEntries, context.l10n.debugLog_noEntries,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -103,7 +103,7 @@ class AppDebugLogScreen extends StatelessWidget {
context.l10n.debugLog_enableInSettings, context.l10n.debugLog_enableInSettings,
style: TextStyle( style: TextStyle(
fontSize: 12, 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) { switch (level) {
case AppDebugLogLevel.info: 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: case AppDebugLogLevel.warning:
return const Icon( return Icon(
Icons.warning_amber_outlined, Icons.warning_amber_outlined,
size: 18, size: 18,
color: Colors.orange, color: colorScheme.tertiary,
); );
case AppDebugLogLevel.error: case AppDebugLogLevel.error:
return const Icon(Icons.error_outline, size: 18, color: Colors.red); return Icon(Icons.error_outline, size: 18, color: colorScheme.error);
} }
} }
} }
+58 -87
View File
@@ -92,11 +92,29 @@ class AppSettingsScreen extends StatelessWidget {
ListTile( ListTile(
leading: const Icon(Icons.brightness_6_outlined), leading: const Icon(Icons.brightness_6_outlined),
title: Text(context.l10n.appSettings_theme), title: Text(context.l10n.appSettings_theme),
subtitle: Text( subtitle: Padding(
_themeModeLabel(context, settingsService.settings.themeMode), padding: const EdgeInsets.only(top: 8),
child: SegmentedButton<String>(
segments: [
ButtonSegment(
value: 'system',
label: Text(context.l10n.appSettings_themeSystem),
),
ButtonSegment(
value: 'light',
label: Text(context.l10n.appSettings_themeLight),
),
ButtonSegment(
value: 'dark',
label: Text(context.l10n.appSettings_themeDark),
),
],
selected: {settingsService.settings.themeMode},
onSelectionChanged: (selection) {
settingsService.setThemeMode(selection.first);
},
),
), ),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeModeDialog(context, settingsService),
), ),
const Divider(height: 1), const Divider(height: 1),
ListTile( ListTile(
@@ -111,18 +129,6 @@ class AppSettingsScreen extends StatelessWidget {
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(context, settingsService), 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, Icons.message_outlined,
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
title: Text( title: Text(
context.l10n.appSettings_messageNotifications, context.l10n.appSettings_messageNotifications,
style: TextStyle( style: TextStyle(
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
), ),
subtitle: Text( subtitle: Text(
@@ -204,7 +210,7 @@ class AppSettingsScreen extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
), ),
value: settingsService.settings.notifyOnNewMessage, value: settingsService.settings.notifyOnNewMessage,
@@ -220,14 +226,14 @@ class AppSettingsScreen extends StatelessWidget {
Icons.forum_outlined, Icons.forum_outlined,
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
title: Text( title: Text(
context.l10n.appSettings_channelMessageNotifications, context.l10n.appSettings_channelMessageNotifications,
style: TextStyle( style: TextStyle(
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
), ),
subtitle: Text( subtitle: Text(
@@ -235,7 +241,7 @@ class AppSettingsScreen extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
), ),
value: settingsService.settings.notifyOnNewChannelMessage, value: settingsService.settings.notifyOnNewChannelMessage,
@@ -251,14 +257,14 @@ class AppSettingsScreen extends StatelessWidget {
Icons.cell_tower, Icons.cell_tower,
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
title: Text( title: Text(
context.l10n.appSettings_advertisementNotifications, context.l10n.appSettings_advertisementNotifications,
style: TextStyle( style: TextStyle(
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
), ),
subtitle: Text( subtitle: Text(
@@ -266,7 +272,7 @@ class AppSettingsScreen extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: settingsService.settings.notificationsEnabled color: settingsService.settings.notificationsEnabled
? null ? null
: Colors.grey, : Theme.of(context).disabledColor,
), ),
), ),
value: settingsService.settings.notifyOnNewAdvert, value: settingsService.settings.notifyOnNewAdvert,
@@ -343,7 +349,13 @@ class AppSettingsScreen extends StatelessWidget {
); );
}, },
), ),
if (settingsService.settings.autoRouteRotationEnabled) ...[ if (settingsService.settings.autoRouteRotationEnabled)
Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
padding: const EdgeInsets.only(left: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1), const Divider(height: 1),
ListTile( ListTile(
title: Text(context.l10n.appSettings_maxRouteWeight), title: Text(context.l10n.appSettings_maxRouteWeight),
@@ -455,6 +467,20 @@ 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( SwitchListTile(
secondary: Icon( secondary: Icon(
Icons.auto_awesome_outlined, Icons.auto_awesome_outlined,
color: translationEnabled ? null : Colors.grey, color: translationEnabled ? null : Theme.of(context).disabledColor,
), ),
title: Text( title: Text(
context.l10n.translation_autoIncomingTitle, context.l10n.translation_autoIncomingTitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey), style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
), ),
subtitle: Text( subtitle: Text(
context.l10n.translation_autoIncomingSubtitle, context.l10n.translation_autoIncomingSubtitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey), style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
), ),
value: settings.autoTranslateIncomingMessages, value: settings.autoTranslateIncomingMessages,
onChanged: translationEnabled onChanged: translationEnabled
@@ -603,15 +629,15 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile( SwitchListTile(
secondary: Icon( secondary: Icon(
Icons.outgoing_mail, Icons.outgoing_mail,
color: translationEnabled ? null : Colors.grey, color: translationEnabled ? null : Theme.of(context).disabledColor,
), ),
title: Text( title: Text(
context.l10n.translation_composerTitle, context.l10n.translation_composerTitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey), style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
), ),
subtitle: Text( subtitle: Text(
context.l10n.translation_composerSubtitle, context.l10n.translation_composerSubtitle,
style: TextStyle(color: translationEnabled ? null : Colors.grey), style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
), ),
value: settings.composerTranslationEnabled, value: settings.composerTranslationEnabled,
onChanged: translationEnabled 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<String>(
groupValue: settingsService.settings.themeMode,
onChanged: (value) {
if (value != null) {
settingsService.setThemeMode(value);
Navigator.pop(context);
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<String>(
title: Text(context.l10n.appSettings_themeSystem),
value: 'system',
),
RadioListTile<String>(
title: Text(context.l10n.appSettings_themeLight),
value: 'light',
),
RadioListTile<String>(
title: Text(context.l10n.appSettings_themeDark),
value: 'dark',
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
),
);
}
String _themeModeLabel(BuildContext context, String value) {
switch (value) {
case 'light':
return context.l10n.appSettings_themeLight;
case 'dark':
return context.l10n.appSettings_themeDark;
default:
return context.l10n.appSettings_themeSystem;
}
}
String _languageLabel(BuildContext context, String? languageCode) { String _languageLabel(BuildContext context, String? languageCode) {
switch (languageCode) { switch (languageCode) {
case 'en': case 'en':
+152 -181
View File
@@ -5,7 +5,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart' hide TextDirection;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
@@ -25,8 +25,9 @@ import '../models/translation_support.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart'; import '../services/chat_text_scale_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
import '../utils/emoji_utils.dart'; import '../helpers/contact_ui.dart';
import '../widgets/byte_count_input.dart'; import '../widgets/byte_count_input.dart';
import '../widgets/empty_state.dart';
import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/emoji_picker.dart'; import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart'; import '../widgets/gif_message.dart';
@@ -283,6 +284,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
) )
: widget.channel.name, : widget.channel.name,
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
Consumer<MeshCoreConnector>( Consumer<MeshCoreConnector>(
builder: (context, connector, _) { builder: (context, connector, _) {
@@ -311,9 +314,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onSelected: (value) { onSelected: (value) {
if (value == 'clearChat') { if (value == 'clearChat') {
context.read<MeshCoreConnector>().clearMessagesForChannel( _confirmClearChat();
widget.channel.index,
);
} }
}, },
itemBuilder: (context) => [ itemBuilder: (context) => [
@@ -321,11 +322,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
value: 'clearChat', value: 'clearChat',
child: Row( child: Row(
children: [ 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), const SizedBox(width: 12),
Text( Text(
context.l10n.contact_clearChat, 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<ChannelChatScreen> {
final messages = connector.getChannelMessages(widget.channel); final messages = connector.getChannelMessages(widget.channel);
if (messages.isEmpty) { if (messages.isEmpty) {
return Center( return EmptyState(
child: Column( icon: widget.channel.isPublicChannel
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.channel.isPublicChannel
? Icons.public ? Icons.public
: Icons.tag, : Icons.tag,
size: 64, title: context.l10n.chat_noMessages,
color: Colors.grey[400], subtitle: context.l10n.chat_sendMessageTo(
), widget.channel.name.isEmpty
const SizedBox(height: 16), ? context.l10n.channels_channelIndex(
Text( widget.channel.index,
context.l10n.chat_noMessages, )
style: TextStyle( : widget.channel.name,
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
context.l10n.chat_sendMessageToStart,
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
), ),
); );
} }
@@ -381,6 +371,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final itemCount = final itemCount =
reversedMessages.length + (_isLoadingOlder ? 1 : 0); reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Prune stale keys (deleted/cleared messages) to avoid
// unbounded growth.
final liveIds = reversedMessages
.map((m) => m.messageId)
.toSet();
_messageKeys.removeWhere((id, _) => !liveIds.contains(id));
// Two messages can collide on messageId (same ms + name/text
// hash). Only the first occurrence owns the shared GlobalKey
// used for scroll-to-message; duplicates get a local key so
// no two widgets share one GlobalKey.
final seenIds = <String>{};
final keyedIndices = <int>{};
for (var i = 0; i < reversedMessages.length; i++) {
if (seenIds.add(reversedMessages[i].messageId)) {
keyedIndices.add(i);
}
}
// Auto-scroll to bottom if user is already at bottom // Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_channelSkipNextBottomSnap) { if (_channelSkipNextBottomSnap) {
@@ -416,14 +425,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
} }
final messageIndex = index; final messageIndex = index;
final message = reversedMessages[messageIndex]; final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) { final GlobalKey messageKey;
_messageKeys[message.messageId] = GlobalKey(); if (keyedIndices.contains(messageIndex)) {
messageKey = _messageKeys.putIfAbsent(
message.messageId,
GlobalKey.new,
);
} else {
messageKey = GlobalKey();
} }
final isUnreadAnchor = final isUnreadAnchor =
_unreadDividerMessageId != null && _unreadDividerMessageId != null &&
message.messageId == _unreadDividerMessageId; message.messageId == _unreadDividerMessageId;
return Container( return Container(
key: _messageKeys[message.messageId]!, key: messageKey,
child: Builder( child: Builder(
builder: (context) { builder: (context) {
final textScale = context final textScale = context
@@ -495,7 +510,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const maxSwipeOffset = 64.0; const maxSwipeOffset = 64.0;
const replySwipeThreshold = 64.0; const replySwipeThreshold = 64.0;
const bodyFontSize = 14.0; const bodyFontSize = 14.0;
final messageBody = Column( final messageBody = LayoutBuilder(
builder: (context, constraints) => Column(
crossAxisAlignment: isOutgoing crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end ? CrossAxisAlignment.end
: CrossAxisAlignment.start, : CrossAxisAlignment.start,
@@ -512,9 +528,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
], ],
Flexible( Flexible(
child: GestureDetector( child: GestureDetector(
onTap: PlatformInfo.isDesktop
? null
: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message), onLongPress: () => _showMessageActions(message),
onSecondaryTapUp: PlatformInfo.isDesktop onSecondaryTapUp: PlatformInfo.isDesktop
? (_) => _showMessageActions(message) ? (_) => _showMessageActions(message)
@@ -524,7 +537,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
? const EdgeInsets.all(4) ? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), : const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65, maxWidth: constraints.maxWidth * 0.65,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isOutgoing color: isOutgoing
@@ -566,20 +579,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
isOutgoing, isOutgoing,
textScale, textScale,
message.senderName, 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) else if (gifId != null)
Stack( Stack(
@@ -599,36 +598,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
.withValues(alpha: 0.6), .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 else
@@ -651,25 +620,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
), ),
), ),
if (!enableTracing && isOutgoing) ...[
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
),
],
], ],
), ),
if (enableTracing) ...[ if (enableTracing && displayPath.isNotEmpty) ...[
if (displayPath.isNotEmpty) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
Padding( Padding(
padding: gifId != null padding: gifId != null
@@ -680,8 +633,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_formatPathPrefixes(displayPath), _formatPathPrefixes(displayPath),
), ),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11 * textScale,
color: Colors.grey[600], color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
), ),
), ),
), ),
@@ -701,48 +656,54 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text( Text(
_formatTime(context, message.timestamp), _formatTime(context, message.timestamp),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11 * textScale,
color: Colors.grey[600], color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
), ),
), ),
if (message.repeatCount > 0) ...[ if (enableTracing && message.repeatCount > 0) ...[
const SizedBox(width: 6), const SizedBox(width: 6),
Icon( Icon(
Icons.repeat, Icons.repeat,
size: 12, size: 12 * textScale,
color: Colors.grey[600], color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
'${message.repeatCount}', '${message.repeatCount}',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11 * textScale,
color: Colors.grey[600], color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
), ),
), ),
], ],
if (isOutgoing) ...[ if (isOutgoing) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
Icon( MessageStatusIcon(
message.status == ChannelMessageStatus.sent isAcked:
? Icons.check
: message.status ==
ChannelMessageStatus.pending
? Icons.schedule
: Icons.error_outline,
size: 14,
color:
message.status == message.status ==
ChannelMessageStatus.failed ChannelMessageStatus.sent,
? Colors.red isRepeated:
: Colors.grey[600], message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isPending:
message.status ==
ChannelMessageStatus.pending,
isFailed:
message.status ==
ChannelMessageStatus.failed,
), ),
], ],
], ],
), ),
), ),
], ],
],
), ),
), ),
), ),
@@ -757,6 +718,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
], ],
], ],
),
); );
if (!isOutgoing && !PlatformInfo.isDesktop) { if (!isOutgoing && !PlatformInfo.isDesktop) {
@@ -958,9 +920,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
IconButton( IconButton(
icon: Icon(Icons.location_on_outlined, color: channelColor), icon: Icon(Icons.location_on_outlined, color: channelColor),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32), constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
onPressed: () { onPressed: () {
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me'; final selfName = context.read<MeshCoreConnector>().selfName ?? context.l10n.chat_me;
final fromName = isOutgoing ? selfName : senderName; final fromName = isOutgoing ? selfName : senderName;
final key = buildSharedMarkerKey( final key = buildSharedMarkerKey(
sourceId: 'channel:${widget.channel.index}', sourceId: 'channel:${widget.channel.index}',
@@ -1020,8 +982,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
} }
Widget _buildAvatar(String senderName) { Widget _buildAvatar(String senderName) {
final initial = _getFirstCharacterOrEmoji(senderName); final initial = firstCharacterOrEmoji(senderName);
final color = _getColorForName(senderName); final color = colorForName(senderName);
return CircleAvatar( return CircleAvatar(
radius: 18, radius: 18,
@@ -1037,36 +999,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
String _getFirstCharacterOrEmoji(String name) {
if (name.isEmpty) return '?';
final emoji = firstEmoji(name);
if (emoji != null) return emoji;
final runes = name.runes.toList();
if (runes.isEmpty) return '?';
return String.fromCharCode(runes[0]).toUpperCase();
}
Color _getColorForName(String name) {
// Generate a consistent color based on the name hash
final hash = name.hashCode;
final colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.amber,
Colors.deepOrange,
];
return colors[hash.abs() % colors.length];
}
Widget _buildReplyBanner(double textScale) { Widget _buildReplyBanner(double textScale) {
final message = _replyingToMessage!; final message = _replyingToMessage!;
return Container( return Container(
@@ -1116,8 +1048,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
icon: const Icon(Icons.close, size: 18), icon: const Icon(Icons.close, size: 18),
onPressed: _cancelReply, onPressed: _cancelReply,
color: Theme.of(context).colorScheme.onSecondaryContainer, color: Theme.of(context).colorScheme.onSecondaryContainer,
padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
constraints: const BoxConstraints(),
), ),
], ],
), ),
@@ -1412,7 +1343,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_setReplyingTo(message); _setReplyingTo(message);
}, },
), ),
if (PlatformInfo.isDesktop)
ListTile( ListTile(
leading: const Icon(Icons.route), leading: const Icon(Icons.route),
title: Text(context.l10n.chat_path), title: Text(context.l10n.chat_path),
@@ -1463,19 +1393,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_markAsUnread(message); _markAsUnread(message);
}, },
), ),
const Divider(),
ListTile( ListTile(
leading: const Icon(Icons.delete_outline), leading: Icon(
title: Text(context.l10n.common_delete), 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 { onTap: () async {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
await _deleteMessage(message); 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<ChannelChatScreen> {
); );
} }
Future<void> _confirmClearChat() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.contact_clearChat),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () => Navigator.pop(dialogContext, true),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
child: Text(context.l10n.common_delete),
),
],
),
);
if (confirmed == true) {
if (!mounted) return;
context.read<MeshCoreConnector>().clearMessagesForChannel(
widget.channel.index,
);
}
}
Future<void> _deleteMessage(ChannelMessage message) async { Future<void> _deleteMessage(ChannelMessage message) async {
await context.read<MeshCoreConnector>().deleteChannelMessage(message); await context.read<MeshCoreConnector>().deleteChannelMessage(message);
if (!mounted) return; if (!mounted) return;
@@ -1557,6 +1517,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
double _maxSwipeDistance = 0; double _maxSwipeDistance = 0;
int? _swipePointerId; int? _swipePointerId;
bool _swipeLockedToHorizontal = false; bool _swipeLockedToHorizontal = false;
bool _isRtl = false;
void _handleSwipeStart(Offset position) { void _handleSwipeStart(Offset position) {
_swipeStartPosition = position; _swipeStartPosition = position;
@@ -1577,11 +1538,13 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
return; 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; const axisLockThreshold = 12.0;
if (!_swipeLockedToHorizontal) { if (!_swipeLockedToHorizontal) {
if (-dx < axisLockThreshold) { if (signedDx < axisLockThreshold) {
return; return;
} }
_swipeLockedToHorizontal = true; _swipeLockedToHorizontal = true;
@@ -1593,28 +1556,32 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
void _handleSwipeUpdate(Offset position) { void _handleSwipeUpdate(Offset position) {
if (_swipeStartPosition == null) return; if (_swipeStartPosition == null) return;
final dx = position.dx - _swipeStartPosition!.dx; final rawDx = position.dx - _swipeStartPosition!.dx;
if (dx >= 0) return; final signedDx = _isRtl ? rawDx : -rawDx;
if (signedDx <= 0) return;
if (-dx < 6) return; if (signedDx < 6) return;
if (-dx > _maxSwipeDistance) { if (signedDx > _maxSwipeDistance) {
_maxSwipeDistance = -dx; _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); final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
if (adjusted != _swipeOffset) { // Translate in the gesture direction: negative for LTR (left), positive for RTL (right).
setState(() => _swipeOffset = adjusted); final translationOffset = _isRtl ? adjusted : -adjusted;
if (translationOffset != _swipeOffset) {
setState(() => _swipeOffset = translationOffset);
} }
} }
void _handleSwipePointerUp(Offset position) { void _handleSwipePointerUp(Offset position) {
if (_swipeLockedToHorizontal && _swipeStartPosition != null) { 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( final peak = math.max(
_maxSwipeDistance, _maxSwipeDistance,
(-dx).clamp(0.0, double.infinity), signedDx.clamp(0.0, double.infinity),
); );
if (peak >= widget.replySwipeThreshold) { if (peak >= widget.replySwipeThreshold) {
widget.onReplyTriggered(); widget.onReplyTriggered();
@@ -1654,6 +1621,10 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
@override @override
Widget build(BuildContext context) { 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( return Listener(
onPointerDown: _handleSwipePointerDown, onPointerDown: _handleSwipePointerDown,
onPointerMove: _handleSwipePointerMove, onPointerMove: _handleSwipePointerMove,
@@ -1667,7 +1638,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
Positioned.fill( Positioned.fill(
child: Opacity( child: Opacity(
opacity: _swipeOffset.abs() / widget.maxSwipeOffset, opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
child: widget.hintBuilder(isStart: false), child: widget.hintBuilder(isStart: hintIsStart),
), ),
), ),
AnimatedContainer( AnimatedContainer(
+43 -13
View File
@@ -106,7 +106,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
if (!hasHopDetails) if (!hasHopDetails)
Text( Text(
l10n.channelPath_noHopDetails, l10n.channelPath_noHopDetails,
style: const TextStyle(color: Colors.grey), style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
) )
else else
..._buildHopTiles(context, hops), ..._buildHopTiles(context, hops),
@@ -131,22 +131,25 @@ class ChannelMessagePathScreen extends StatelessWidget {
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildDetailRow(l10n.channelPath_senderLabel, message.senderName), _buildDetailRow(context, l10n.channelPath_senderLabel, message.senderName),
_buildDetailRow( _buildDetailRow(
context,
l10n.channelPath_timeLabel, l10n.channelPath_timeLabel,
_formatTime(message.timestamp, l10n), _formatTime(message.timestamp, l10n),
), ),
if (message.repeatCount > 0) if (message.repeatCount > 0)
_buildDetailRow( _buildDetailRow(
context,
l10n.channelPath_repeatsLabel, l10n.channelPath_repeatsLabel,
message.repeatCount.toString(), message.repeatCount.toString(),
), ),
_buildDetailRow( _buildDetailRow(
context,
l10n.channelPath_pathLabelTitle, l10n.channelPath_pathLabelTitle,
_formatPathLabel(message.pathLength, l10n), _formatPathLabel(message.pathLength, l10n),
), ),
if (observedLabel != null) 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); return l10n.channelPath_observedSomeOf(observedCount, pathLength);
} }
Widget _buildDetailRow(String label, String value) { Widget _buildDetailRow(BuildContext context, String label, String value) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 2), padding: const EdgeInsets.symmetric(vertical: 2),
child: Row( child: Row(
@@ -258,7 +261,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
children: [ children: [
SizedBox( SizedBox(
width: 70, 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)), Expanded(child: Text(value)),
], ],
@@ -412,17 +415,17 @@ class _ChannelMessagePathMapScreenState
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: 'Zoom in', tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1), onPressed: () => _zoomMapBy(1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
tooltip: 'Zoom out', tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1), onPressed: () => _zoomMapBy(-1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.my_location), icon: const Icon(Icons.my_location),
tooltip: 'Center map', tooltip: context.l10n.map_centerMap,
onPressed: () => _resetMapView( onPressed: () => _resetMapView(
initialCenter: initialCenter, initialCenter: initialCenter,
initialZoom: initialZoom, initialZoom: initialZoom,
@@ -593,7 +596,7 @@ class _ChannelMessagePathMapScreenState
if (points.isEmpty) if (points.isEmpty)
Center( Center(
child: Card( child: Card(
color: Colors.white.withValues(alpha: 0.9), color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9),
child: Padding( child: Padding(
padding: EdgeInsets.all(12), padding: EdgeInsets.all(12),
child: Text( child: Text(
@@ -664,7 +667,7 @@ class _ChannelMessagePathMapScreenState
label, label,
_formatPathPrefixes(selectedPath.pathBytes), _formatPathPrefixes(selectedPath.pathBytes),
), ),
style: TextStyle(color: Colors.grey[700], fontSize: 12), style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12),
), ),
], ],
), ),
@@ -685,9 +688,12 @@ class _ChannelMessagePathMapScreenState
markers.add( markers.add(
Marker( Marker(
point: point, point: point,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35, width: 35,
height: 35, height: 35,
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.green, color: Colors.green,
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -711,6 +717,7 @@ class _ChannelMessagePathMapScreenState
), ),
), ),
), ),
),
); );
if (showLabels) { if (showLabels) {
markers.add( markers.add(
@@ -729,9 +736,12 @@ class _ChannelMessagePathMapScreenState
markers.add( markers.add(
Marker( Marker(
point: selfPoint, point: selfPoint,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35, width: 35,
height: 35, height: 35,
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.teal, color: Colors.teal,
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -755,6 +765,7 @@ class _ChannelMessagePathMapScreenState
), ),
), ),
), ),
),
); );
if (showLabels) { if (showLabels) {
markers.add( markers.add(
@@ -804,6 +815,12 @@ class _ChannelMessagePathMapScreenState
); );
} }
Widget _colorDot(Color color) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
Widget _buildLegendCard( Widget _buildLegendCard(
BuildContext context, BuildContext context,
List<_PathHop> hops, List<_PathHop> hops,
@@ -826,10 +843,23 @@ class _ChannelMessagePathMapScreenState
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Text( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}', '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600), 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), const Divider(height: 1),
Expanded( Expanded(
+89 -46
View File
@@ -111,14 +111,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
const Icon(Icons.logout, color: Colors.red), Icon(
Icons.logout,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(context.l10n.common_disconnect), Text(context.l10n.common_disconnect),
], ],
), ),
onTap: () => _disconnect(context), onTap: () => _disconnect(context),
), ),
if (_communities.isNotEmpty)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -241,32 +243,22 @@ class _ChannelsScreenState extends State<ChannelsScreen>
), ),
Expanded( Expanded(
child: filteredChannels.isEmpty child: filteredChannels.isEmpty
? ListView( ? LayoutBuilder(
builder: (context, constraints) => ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [ children: [
SizedBox( ConstrainedBox(
height: MediaQuery.of(context).size.height - 300, constraints: BoxConstraints(
child: Center( minHeight: constraints.maxHeight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
), ),
const SizedBox(height: 16), child: EmptyState(
Text( icon: Icons.search_off,
title:
context.l10n.channels_noChannelsFound, context.l10n.channels_noChannelsFound,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
), ),
), ),
], ],
), ),
),
),
],
) )
: (viewState.channelsSortOption == : (viewState.channelsSortOption ==
ChannelSortOption.manual && ChannelSortOption.manual &&
@@ -357,6 +349,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
int? dragIndex, int? dragIndex,
}) { }) {
final unreadCount = connector.getUnreadCountForChannel(channel); final unreadCount = connector.getUnreadCountForChannel(channel);
final isMuted = context.watch<AppSettingsService>().isChannelMuted(
channel.name,
);
// Determine icon and colors based on channel type // Determine icon and colors based on channel type
IconData icon; IconData icon;
@@ -449,27 +444,36 @@ class _ChannelsScreenState extends State<ChannelsScreen>
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (isMuted) ...[
Icon(
Icons.notifications_off,
size: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
],
if (unreadCount > 0) ...[ if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount), UnreadBadge(count: unreadCount),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
if (showDragHandle && dragIndex != null) if (showDragHandle && dragIndex != null)
ReorderableDelayedDragStartListener( ReorderableDragStartListener(
index: dragIndex, index: dragIndex,
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon( child: Icon(
Icons.drag_handle, Icons.drag_handle,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
),
], ],
), ),
onTap: () async { onTap: () {
final unread = connector.getUnreadCountForChannelIndex( final unread = connector.getUnreadCountForChannelIndex(
channel.index, channel.index,
); );
connector.markChannelRead(channel.index); connector.markChannelRead(channel.index);
await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -479,7 +483,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
), ),
), ),
); );
}
}, },
onLongPress: () => _showChannelActions( onLongPress: () => _showChannelActions(
context, context,
@@ -540,10 +543,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red), leading: Icon(
Icons.delete_outline,
color: Theme.of(sheetContext).colorScheme.error,
),
title: Text( title: Text(
context.l10n.channels_deleteChannel, context.l10n.channels_deleteChannel,
style: const TextStyle(color: Colors.red), style: TextStyle(
color: Theme.of(sheetContext).colorScheme.error,
),
), ),
onTap: () async { onTap: () async {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
@@ -725,23 +733,39 @@ class _ChannelsScreenState extends State<ChannelsScreen>
? (isSelected ? (isSelected
? Theme.of(dialogContext).colorScheme.primaryContainer ? Theme.of(dialogContext).colorScheme.primaryContainer
: null) : null)
: Colors.grey.withValues(alpha: 0.2), : Theme.of(
dialogContext,
).colorScheme.onSurface.withValues(alpha: 0.12),
child: Icon( child: Icon(
icon, icon,
color: enabled color: enabled
? (isSelected ? (isSelected
? Theme.of(dialogContext).colorScheme.primary ? Theme.of(dialogContext).colorScheme.primary
: null) : null)
: Colors.grey, : Theme.of(
dialogContext,
).colorScheme.onSurface.withValues(alpha: 0.38),
), ),
), ),
title: Text( title: Text(
title, 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: Text(
subtitle, 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, trailing: enabled ? const Icon(Icons.chevron_right) : null,
selected: isSelected, selected: isSelected,
@@ -929,7 +953,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Channel.publicChannelPsk, Channel.publicChannelPsk,
); );
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.setChannel(nextIndex, 'Public', psk); connector.setChannel(nextIndex, context.l10n.channels_public, psk);
if (context.mounted) { if (context.mounted) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
@@ -1043,7 +1067,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
dialogContext.l10n.community_hashtagPrivacyHint, dialogContext.l10n.community_hashtagPrivacyHint,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Theme.of(
dialogContext,
).colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
), ),
), ),
@@ -1214,6 +1240,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: FilledButton( child: FilledButton(
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
final publicLabel = context.l10n.channels_public;
if (name.isEmpty) { if (name.isEmpty) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
@@ -1238,7 +1265,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final psk = community final psk = community
.deriveCommunityPublicPsk(); .deriveCommunityPublicPsk();
final channelName = final channelName =
'${community.name} Public'; '${community.name} $publicLabel';
connector.setChannel( connector.setChannel(
nextIndex, nextIndex,
channelName, channelName,
@@ -1594,7 +1621,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}, },
child: Text( child: Text(
dialogContext.l10n.common_delete, 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<ChannelsScreen>
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) { void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
final psk = Channel.parsePskHex(Channel.publicChannelPsk); final psk = Channel.parsePskHex(Channel.publicChannelPsk);
connector.setChannel(0, 'Public', psk); connector.setChannel(0, context.l10n.channels_public, psk);
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.channels_publicChannelAdded), content: Text(context.l10n.channels_publicChannelAdded),
@@ -1653,14 +1680,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Icon( Icon(
Icons.groups_outlined, Icons.groups_outlined,
size: 64, size: 64,
color: Colors.grey[400], color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.6),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
context.l10n.community_noCommunities, context.l10n.community_noCommunities,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey[600], color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -1668,7 +1700,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
context.l10n.community_scanOrCreate, context.l10n.community_scanOrCreate,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[500], color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.8),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -1692,10 +1727,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
), ),
title: Text(community.name), title: Text(community.name),
subtitle: Text( subtitle: Text(
'ID: ${community.shortCommunityId}...', context.l10n.channels_communityShortId(community.shortCommunityId),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
), ),
), ),
trailing: PopupMenuButton<String>( trailing: PopupMenuButton<String>(
@@ -1722,14 +1759,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
value: 'leave', value: 'leave',
child: Row( child: Row(
children: [ children: [
const Icon( Icon(
Icons.exit_to_app, Icons.exit_to_app,
color: Colors.red, color: Theme.of(
context,
).colorScheme.error,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
context.l10n.community_delete, 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<ChannelsScreen>
}, },
child: Text( child: Text(
dialogContext.l10n.community_delete, dialogContext.l10n.community_delete,
style: const TextStyle(color: Colors.red), style: TextStyle(color: Theme.of(context).colorScheme.error),
), ),
), ),
], ],
File diff suppressed because it is too large Load Diff
+12 -21
View File
@@ -8,34 +8,25 @@ class ChromeRequiredScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
final theme = Theme.of(context); final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark; final colorScheme = theme.colorScheme;
return Scaffold( return Scaffold(
body: Container( body: Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: BoxDecoration( color: colorScheme.surface,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
),
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1), color: colorScheme.tertiaryContainer.withValues(alpha: 0.4),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: Icon(
Icons.browser_not_supported_rounded, Icons.browser_not_supported_rounded,
size: 80, size: 80,
color: Colors.orange, color: colorScheme.tertiary,
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
@@ -44,7 +35,7 @@ class ChromeRequiredScreen extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith( style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -52,7 +43,7 @@ class ChromeRequiredScreen extends StatelessWidget {
l10n.scanner_chromeRequiredMessage, l10n.scanner_chromeRequiredMessage,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.white70 : Colors.black54, color: colorScheme.onSurfaceVariant,
height: 1.5, height: 1.5,
), ),
), ),
@@ -62,19 +53,19 @@ class ChromeRequiredScreen extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1), color: colorScheme.secondaryContainer.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(30), 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( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.info_outline, size: 20, color: Colors.blue), Icon(Icons.info_outline, size: 20, color: colorScheme.secondary),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
"Web Bluetooth requires a Chromium browser", l10n.chrome_bluetoothRequiresChromium,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.blue, color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -210,10 +210,10 @@ class _NoiseChartPainter extends CustomPainter {
} }
final span = maxV - minV; final span = maxV - minV;
for (var i = 0; i <= 2; i++) { for (var i = 0; i <= 4; i++) {
final v = maxV - span * i / 2; final v = maxV - span * i / 4;
final tp = _yAxisLabel(v); 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)); tp.paint(canvas, Offset(4, y));
} }
+184 -128
View File
@@ -29,6 +29,7 @@ import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart'; import '../widgets/room_login_dialog.dart';
import '../widgets/sync_progress_overlay.dart'; import '../widgets/sync_progress_overlay.dart';
import '../widgets/unread_badge.dart'; import '../widgets/unread_badge.dart';
import '../helpers/contact_ui.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart'; import 'channels_screen.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
@@ -59,7 +60,7 @@ class _ContactsScreenState extends State<ContactsScreen>
String _loadedGroupScopeKeyHex = ''; String _loadedGroupScopeKeyHex = '';
Timer? _searchDebounce; Timer? _searchDebounce;
final Set<ContactOperationType> _pendingOperations = {}; final List<ContactOperationType> _pendingOperations = [];
StreamSubscription<Uint8List>? _frameSubscription; StreamSubscription<Uint8List>? _frameSubscription;
@@ -185,59 +186,52 @@ class _ContactsScreenState extends State<ContactsScreen>
Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); 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) { if (code == respCodeOk) {
// Show a snackbar indicating success
if (!mounted) return; if (!mounted) return;
if (_pendingOperations.isEmpty) return;
if (_pendingOperations.contains(ContactOperationType.import)) { final op = _pendingOperations.removeAt(0);
switch (op) {
case ContactOperationType.import:
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.contacts_contactImported), content: Text(context.l10n.contacts_contactImported),
); );
} case ContactOperationType.zeroHopShare:
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.contacts_zeroHopContactAdvertSent), content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
); );
} case ContactOperationType.export:
if (_pendingOperations.contains(ContactOperationType.export)) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.contacts_contactAdvertCopied), content: Text(context.l10n.contacts_contactAdvertCopied),
); );
} }
_pendingOperations.clear();
} }
if (code == respCodeErr) { if (code == respCodeErr) {
// Show a snackbar indicating failure
if (!mounted) return; if (!mounted) return;
if (_pendingOperations.isEmpty) return;
if (_pendingOperations.contains(ContactOperationType.import)) { final op = _pendingOperations.removeAt(0);
switch (op) {
case ContactOperationType.import:
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.contacts_contactImportFailed), content: Text(context.l10n.contacts_contactImportFailed),
); );
} case ContactOperationType.zeroHopShare:
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
); );
} case ContactOperationType.export:
if (_pendingOperations.contains(ContactOperationType.export)) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.contacts_contactAdvertCopyFailed), content: Text(context.l10n.contacts_contactAdvertCopyFailed),
); );
} }
_pendingOperations.clear();
} }
} catch (e) { } catch (e) {
appLogger.error( appLogger.error(
@@ -252,17 +246,37 @@ class _ContactsScreenState extends State<ContactsScreen>
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactFrame = buildExportContactFrame(pubKey); final exportContactFrame = buildExportContactFrame(pubKey);
_pendingOperations.add(ContactOperationType.export); _pendingOperations.add(ContactOperationType.export);
try {
await connector.sendFrame(exportContactFrame, expectsGenericAck: true); await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
} catch (e) {
_pendingOperations.remove(ContactOperationType.export);
if (mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
);
}
}
} }
Future<void> _contactZeroHop(Uint8List pubKey) async { Future<void> _contactZeroHop(Uint8List pubKey) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactZeroHopFrame = buildZeroHopContact(pubKey); final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
_pendingOperations.add(ContactOperationType.zeroHopShare); _pendingOperations.add(ContactOperationType.zeroHopShare);
try {
await connector.sendFrame( await connector.sendFrame(
exportContactZeroHopFrame, exportContactZeroHopFrame,
expectsGenericAck: true, expectsGenericAck: true,
); );
} catch (e) {
_pendingOperations.remove(ContactOperationType.zeroHopShare);
if (mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
);
}
}
} }
Future<void> _contactImport() async { Future<void> _contactImport() async {
@@ -288,11 +302,10 @@ class _ContactsScreenState extends State<ContactsScreen>
return; return;
} }
final hexString = text.substring('meshcore://'.length); final hexString = text.substring('meshcore://'.length);
final Uint8List importContactFrame;
try { try {
final bytes = hex2Uint8List(hexString); final bytes = hex2Uint8List(hexString);
final importContactFrame = buildImportContactFrame(bytes); importContactFrame = buildImportContactFrame(bytes);
_pendingOperations.add(ContactOperationType.import);
connector.importContact(importContactFrame);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showDismissibleSnackBar( showDismissibleSnackBar(
@@ -300,6 +313,19 @@ class _ContactsScreenState extends State<ContactsScreen>
content: Text(context.l10n.contacts_invalidAdvertFormat), content: Text(context.l10n.contacts_invalidAdvertFormat),
); );
} }
return;
}
_pendingOperations.add(ContactOperationType.import);
try {
await connector.sendFrame(importContactFrame, expectsGenericAck: true);
} catch (e) {
_pendingOperations.remove(ContactOperationType.import);
if (mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactImportFailed),
);
}
} }
} }
@@ -322,7 +348,34 @@ class _ContactsScreenState extends State<ContactsScreen>
bottom: const SyncProgressAppBarBottom(), bottom: const SyncProgressAppBarBottom(),
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
itemBuilder: (context) => [ tooltip: context.l10n.contacts_moreOptions,
itemBuilder: (context) => <PopupMenuEntry<dynamic>>[
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.person_add_rounded),
const SizedBox(width: 8),
Text(context.l10n.discoveredContacts_Title),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
),
onTap: () => _contactImport(),
),
const PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -365,46 +418,20 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
onTap: () => _contactExport(Uint8List.fromList([])), onTap: () => _contactExport(Uint8List.fromList([])),
), ),
const PopupMenuDivider(),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
const Icon(Icons.paste), Icon(
const SizedBox(width: 8), Icons.logout,
Text(context.l10n.contacts_addContactFromClipboard), color: Theme.of(context).colorScheme.error,
],
), ),
onTap: () => _contactImport(),
),
],
icon: const Icon(Icons.connect_without_contact),
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(context.l10n.common_disconnect), Text(context.l10n.common_disconnect),
], ],
), ),
onTap: () => _disconnect(context, connector), 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( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -426,6 +453,10 @@ class _ContactsScreenState extends State<ContactsScreen>
], ],
), ),
body: _buildContactsBody(context, connector), body: _buildContactsBody(context, connector),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddContactSheet(context),
child: const Icon(Icons.person_add),
),
bottomNavigationBar: SafeArea( bottomNavigationBar: SafeArea(
top: false, top: false,
child: QuickSwitchBar( child: QuickSwitchBar(
@@ -440,6 +471,40 @@ class _ContactsScreenState extends State<ContactsScreen>
); );
} }
void _showAddContactSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.paste),
title: Text(context.l10n.contacts_addContactFromClipboard),
onTap: () {
Navigator.pop(sheetContext);
_contactImport();
},
),
ListTile(
leading: const Icon(Icons.person_add_rounded),
title: Text(context.l10n.discoveredContacts_Title),
onTap: () {
Navigator.pop(sheetContext);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
);
},
),
],
),
),
);
}
Future<void> _disconnect( Future<void> _disconnect(
BuildContext context, BuildContext context,
MeshCoreConnector connector, MeshCoreConnector connector,
@@ -571,7 +636,11 @@ class _ContactsScreenState extends State<ContactsScreen>
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton( IconButton(
tooltip: menuContext.l10n.contacts_deleteGroup, 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 onPressed: canManageGroups
? () => _closeDropdownAndRun( ? () => _closeDropdownAndRun(
menuContext, menuContext,
@@ -589,12 +658,20 @@ class _ContactsScreenState extends State<ContactsScreen>
], ],
child: SizedBox( child: SizedBox(
height: 48, height: 48,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(12),
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Text(selectedGroupName, overflow: TextOverflow.ellipsis), child: Text(
selectedGroupName,
overflow: TextOverflow.ellipsis,
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),
@@ -602,6 +679,7 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
), ),
), ),
),
); );
} }
@@ -624,6 +702,16 @@ class _ContactsScreenState extends State<ContactsScreen>
icon: Icons.people_outline, icon: Icons.people_outline,
title: context.l10n.contacts_noContacts, title: context.l10n.contacts_noContacts,
subtitle: context.l10n.contacts_contactsWillAppear, 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<ContactsScreen>
width: 48, width: 48,
height: 48, height: 48,
child: IconButton( child: IconButton(
tooltip: viewState.contactsSearchExpanded
? context.l10n.contacts_searchClose
: context.l10n.contacts_searchOpen,
onPressed: () { onPressed: () {
if (viewState.contactsSearchExpanded) { if (viewState.contactsSearchExpanded) {
_collapseContactsSearch(viewState); _collapseContactsSearch(viewState);
@@ -791,25 +882,29 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
), ),
Expanded( Expanded(
child: RefreshIndicator(
onRefresh: () => connector.getContacts(),
child: filteredAndSorted.isEmpty child: filteredAndSorted.isEmpty
? Center( ? LayoutBuilder(
child: Column( builder: (context, constraints) => ListView(
mainAxisAlignment: MainAxisAlignment.center, physics: const AlwaysScrollableScrollPhysics(),
children: [ children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]), ConstrainedBox(
const SizedBox(height: 16), constraints: BoxConstraints(
Text( minHeight: constraints.maxHeight,
viewState.contactsShowUnreadOnly ),
child: EmptyState(
icon: Icons.search_off,
title: viewState.contactsShowUnreadOnly
? context.l10n.contacts_noUnreadContacts ? context.l10n.contacts_noUnreadContacts
: context.l10n.contacts_noContactsFound, : context.l10n.contacts_noContactsFound,
style: TextStyle(fontSize: 16, color: Colors.grey[600]), ),
), ),
], ],
), ),
) )
: RefreshIndicator( : ListView.builder(
onRefresh: () => connector.getContacts(), padding: const EdgeInsets.only(bottom: 88),
child: ListView.builder(
itemCount: filteredAndSorted.length, itemCount: filteredAndSorted.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final contact = filteredAndSorted[index]; final contact = filteredAndSorted[index];
@@ -1048,7 +1143,7 @@ class _ContactsScreenState extends State<ContactsScreen>
}, },
child: Text( child: Text(
context.l10n.common_delete, 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<ContactsScreen>
); );
}, },
), ),
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
onTap: () {
Navigator.pop(sheetContext);
_openChat(context, contact);
},
),
], ],
ListTile( ListTile(
leading: Icon( leading: Icon(
@@ -1403,10 +1490,15 @@ class _ContactsScreenState extends State<ContactsScreen>
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.delete, color: Colors.red), leading: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.error,
),
title: Text( title: Text(
context.l10n.contacts_deleteContact, context.l10n.contacts_deleteContact,
style: const TextStyle(color: Colors.red), style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
), ),
onTap: () { onTap: () {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
@@ -1441,7 +1533,7 @@ class _ContactsScreenState extends State<ContactsScreen>
}, },
child: Text( child: Text(
context.l10n.common_delete, context.l10n.common_delete,
style: const TextStyle(color: Colors.red), style: TextStyle(color: Theme.of(context).colorScheme.error),
), ),
), ),
], ],
@@ -1473,26 +1565,15 @@ class _ContactTile extends StatelessWidget {
onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null, onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null,
child: ListTile( child: ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type), backgroundColor: contactTypeColor(contact.type),
child: _buildContactAvatar(contact), child: _buildContactAvatar(contact),
), ),
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Column( subtitle: Text(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
contact.pathLabel(context.l10n), contact.pathLabel(context.l10n),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text(
contact.shortPubKeyHex,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
],
),
// Clamp text scaling in trailing section to prevent overflow while // Clamp text scaling in trailing section to prevent overflow while
// maintaining accessibility. Primary content (title/subtitle) scales normally. // maintaining accessibility. Primary content (title/subtitle) scales normally.
trailing: MediaQuery( trailing: MediaQuery(
@@ -1502,7 +1583,7 @@ class _ContactTile extends StatelessWidget {
), ),
), ),
child: SizedBox( child: SizedBox(
width: 120, width: 96,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
@@ -1516,7 +1597,10 @@ class _ContactTile extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: TextStyle(fontSize: 12, color: Colors.grey[600]), style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
), ),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -1529,7 +1613,9 @@ class _ContactTile extends StatelessWidget {
Icon( Icon(
Icons.location_on, Icons.location_on,
size: 14, 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) { if (emoji != null) {
return Text(emoji, style: const TextStyle(fontSize: 18)); return Text(emoji, style: const TextStyle(fontSize: 18));
} }
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20); return Icon(contactTypeIcon(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;
}
} }
String _formatLastSeen(BuildContext context, DateTime lastSeen) { String _formatLastSeen(BuildContext context, DateTime lastSeen) {
+29 -43
View File
@@ -12,6 +12,7 @@ import '../utils/contact_search.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart'; import '../widgets/list_filter_widget.dart';
import '../helpers/contact_ui.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
enum DiscoverySortOption { lastSeen, name, type } enum DiscoverySortOption { lastSeen, name, type }
@@ -71,7 +72,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
const Icon(Icons.delete, color: Colors.red), Icon(
Icons.delete,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(context.l10n.discoveredContacts_deleteContactAll), Text(context.l10n.discoveredContacts_deleteContactAll),
], ],
@@ -99,9 +103,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
final contact = filteredAndSorted[index]; final contact = filteredAndSorted[index];
final tile = ListTile( final tile = ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type), backgroundColor: contactTypeColor(contact.type),
child: Icon( child: Icon(
_getTypeIcon(contact.type), contactTypeIcon(contact.type),
color: Colors.white, color: Colors.white,
size: 20, size: 20,
), ),
@@ -142,7 +146,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
), ),
), ),
Row( Row(
@@ -152,7 +158,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
Icon( Icon(
Icons.location_on, Icons.location_on,
size: 14, size: 14,
color: Colors.grey[400], color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.6),
), ),
if (contact.rawPacket != null) if (contact.rawPacket != null)
const SizedBox(width: 2), const SizedBox(width: 2),
@@ -160,7 +169,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
Icon( Icon(
Icons.cell_tower, Icons.cell_tower,
size: 14, size: 14,
color: Colors.grey[400], color: Theme.of(context)
.colorScheme
.onSurfaceVariant
.withValues(alpha: 0.6),
), ),
], ],
), ),
@@ -170,6 +182,17 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
), ),
onTap: () { onTap: () {
connector.importDiscoveredContact(contact); connector.importDiscoveredContact(contact);
showDismissibleSnackBar(
context,
content: Text(
context.l10n.discoveredContacts_contactAdded,
),
action: SnackBarAction(
label: context.l10n.common_undo,
onPressed: () =>
connector.removeContact(contact),
),
);
}, },
onLongPress: () => onLongPress: () =>
_showContactContextMenu(contact, connector), _showContactContextMenu(contact, connector),
@@ -203,11 +226,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile(
leading: const Icon(Icons.add_reaction_sharp),
title: Text(l10n.discoveredContacts_addContact),
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
),
ListTile( ListTile(
leading: const Icon(Icons.copy), leading: const Icon(Icons.copy),
title: Text(l10n.discoveredContacts_copyContact), title: Text(l10n.discoveredContacts_copyContact),
@@ -227,9 +245,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
if (!mounted || action == null) return; if (!mounted || action == null) return;
switch (action) { switch (action) {
case 'import_contact':
connector.importDiscoveredContact(contact);
break;
case 'copy_contact': case 'copy_contact':
if (contact.rawPacket == null) return; if (contact.rawPacket == null) return;
final hexString = pubKeyToHex(contact.rawPacket!); final hexString = pubKeyToHex(contact.rawPacket!);
@@ -429,35 +444,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
} }
} }
IconData _getTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color _getTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
}
String _formatLastSeen(BuildContext context, DateTime lastSeen) { String _formatLastSeen(BuildContext context, DateTime lastSeen) {
final now = DateTime.now(); final now = DateTime.now();
+24 -16
View File
@@ -73,7 +73,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
double _startAntennaHeight = 5.0; double _startAntennaHeight = 5.0;
double _endAntennaHeight = 5.0; double _endAntennaHeight = 5.0;
bool _showHud = true; bool _showHud = true;
bool _menuExpanded = true; bool _menuExpanded = false;
bool _showDisplayNodes = true; bool _showDisplayNodes = true;
bool _showMarkerLabels = true; bool _showMarkerLabels = true;
bool _didReceivePositionUpdate = false; bool _didReceivePositionUpdate = false;
@@ -159,17 +159,17 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: 'Zoom in', tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1), onPressed: () => _zoomMapBy(1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
tooltip: 'Zoom out', tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1), onPressed: () => _zoomMapBy(-1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.my_location), icon: const Icon(Icons.my_location),
tooltip: 'Center map', tooltip: context.l10n.map_centerMap,
onPressed: () => _resetMapView( onPressed: () => _resetMapView(
initialCenter: initialCenter, initialCenter: initialCenter,
initialZoom: initialZoom, initialZoom: initialZoom,
@@ -224,6 +224,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
setState(() { setState(() {
_result = result; _result = result;
_selectedObstruction = _defaultObstructionFor(result); _selectedObstruction = _defaultObstructionFor(result);
_menuExpanded = true;
}); });
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
@@ -506,7 +507,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
bottom: 12, bottom: 12,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black54, color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Padding( child: Padding(
@@ -516,11 +517,18 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
), ),
child: Text( child: Text(
context.l10n.losElevationAttribution, 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( floatingActionButton: FloatingActionButton(
@@ -623,7 +631,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
context.l10n.losBlockedSpotsHint, 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), const SizedBox(height: 6),
Wrap( Wrap(
@@ -692,7 +700,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
'${_selectedObstruction!.point.longitude.toStringAsFixed(5)}', '${_selectedObstruction!.point.longitude.toStringAsFixed(5)}',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.grey[700], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -711,14 +719,14 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
context.l10n.losFrequencyLabel, context.l10n.losFrequencyLabel,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.grey[700], color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'${displayFrequencyMHz.toStringAsFixed(3)} MHz', '${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) ...[ if (kFactorUsed != null) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -726,7 +734,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
'k=${kFactorUsed.toStringAsFixed(3)}', 'k=${kFactorUsed.toStringAsFixed(3)}',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.grey[700], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@@ -734,7 +742,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
icon: const Icon(Icons.info_outline, size: 16), icon: const Icon(Icons.info_outline, size: 16),
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
tooltip: context.l10n.losFrequencyInfoTooltip, tooltip: context.l10n.losFrequencyInfoTooltip,
onPressed: () { onPressed: () {
_showFrequencyInfoDialog( _showFrequencyInfoDialog(
@@ -750,7 +758,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
), ),
Text( Text(
context.l10n.losElevationAttribution, 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), const SizedBox(height: 6),
ExpansionTile( ExpansionTile(
@@ -1730,12 +1738,12 @@ class _LosLegend extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyle = final textStyle =
Theme.of(context).textTheme.labelSmall?.copyWith( Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white70, color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
) ?? ) ??
const TextStyle( TextStyle(
color: Colors.white70, color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
); );
+4 -4
View File
@@ -83,17 +83,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: 'Zoom in', tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1), onPressed: () => _zoomMapBy(1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
tooltip: 'Zoom out', tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1), onPressed: () => _zoomMapBy(-1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.my_location), icon: const Icon(Icons.my_location),
tooltip: 'Center map', tooltip: context.l10n.map_centerMap,
onPressed: _resetMapView, onPressed: _resetMapView,
), ),
], ],
@@ -458,7 +458,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text( child: Text(
l10n.mapCache_failedDownloads(_failedTiles), l10n.mapCache_failedDownloads(_failedTiles),
style: TextStyle(color: Colors.orange[700]), style: TextStyle(color: Theme.of(context).colorScheme.error),
), ),
), ),
], ],
+124 -91
View File
@@ -183,17 +183,17 @@ class _MapScreenState extends State<MapScreen> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: 'Zoom in', tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1), onPressed: () => _zoomMapBy(1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
tooltip: 'Zoom out', tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1), onPressed: () => _zoomMapBy(-1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.my_location), icon: const Icon(Icons.my_location),
tooltip: 'Center map', tooltip: context.l10n.map_centerMap,
onPressed: () => _mapController.move(center, zoom), onPressed: () => _mapController.move(center, zoom),
), ),
], ],
@@ -417,18 +417,36 @@ class _MapScreenState extends State<MapScreen> {
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
bottom: const SyncProgressAppBarBottom(), bottom: const SyncProgressAppBarBottom(),
actions: [ actions: [
if (!_isBuildingPathTrace) PopupMenuButton(
IconButton( itemBuilder: (context) => [
icon: const Icon(Icons.radar), if (!_isBuildingPathTrace &&
onPressed: () => _startPath( connector.selfLatitude != null &&
LatLng(connector.selfLatitude!, connector.selfLongitude!), connector.selfLongitude != null)
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.radar),
const SizedBox(width: 8),
Text(context.l10n.contacts_pathTrace),
],
),
onTap: () => _startPath(
LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
), ),
tooltip: context.l10n.contacts_pathTrace,
), ),
if (!_isBuildingPathTrace) if (!_isBuildingPathTrace)
IconButton( PopupMenuItem(
icon: const LosIcon(), child: Row(
onPressed: () { children: [
const LosIcon(),
const SizedBox(width: 8),
Text(context.l10n.map_lineOfSight),
],
),
onTap: () {
final candidates = <LineOfSightEndpoint>[]; final candidates = <LineOfSightEndpoint>[];
if (connector.selfLatitude != null && if (connector.selfLatitude != null &&
connector.selfLongitude != null) { connector.selfLongitude != null) {
@@ -464,14 +482,11 @@ class _MapScreenState extends State<MapScreen> {
), ),
); );
}, },
tooltip: context.l10n.map_lineOfSight,
), ),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
const Icon(Icons.logout, color: Colors.red), Icon(Icons.logout, color: Theme.of(context).colorScheme.error),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(context.l10n.common_disconnect), Text(context.l10n.common_disconnect),
], ],
@@ -906,8 +921,8 @@ class _MapScreenState extends State<MapScreen> {
final color = _getNodeColor(guess.contact.type); final color = _getNodeColor(guess.contact.type);
final marker = Marker( final marker = Marker(
point: guess.position, point: guess.position,
width: 35, width: 48,
height: 35, height: 48,
child: GestureDetector( child: GestureDetector(
onLongPress: () => _isBuildingPathTrace onLongPress: () => _isBuildingPathTrace
? _showNodeInfo(context, guess.contact) ? _showNodeInfo(context, guess.contact)
@@ -919,6 +934,7 @@ class _MapScreenState extends State<MapScreen> {
guess.contact, guess.contact,
guessedPosition: guess.position, guessedPosition: guess.position,
), ),
child: Center(
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -942,6 +958,7 @@ class _MapScreenState extends State<MapScreen> {
), ),
), ),
), ),
),
); );
markers.add(marker); markers.add(marker);
@@ -1030,17 +1047,16 @@ class _MapScreenState extends State<MapScreen> {
for (final contact in filteredContacts) { for (final contact in filteredContacts) {
final marker = Marker( final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!), point: LatLng(contact.latitude!, contact.longitude!),
width: 35, width: 48,
height: 35, height: 48,
child: GestureDetector( child: GestureDetector(
onLongPress: () => onLongPress: () =>
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null, _isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
onTap: () => _isBuildingPathTrace onTap: () => _isBuildingPathTrace
? _addToPath(context, contact) ? _addToPath(context, contact)
: _showNodeInfo(context, contact), : _showNodeInfo(context, contact),
child: Column( child: Center(
children: [ child: Container(
Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: settings.mapShowOverlaps && !_isBuildingPathTrace color: settings.mapShowOverlaps && !_isBuildingPathTrace
@@ -1062,7 +1078,6 @@ class _MapScreenState extends State<MapScreen> {
size: 20, size: 20,
), ),
), ),
],
), ),
), ),
); );
@@ -1208,7 +1223,7 @@ class _MapScreenState extends State<MapScreen> {
Icon( Icon(
Icons.location_on, Icons.location_on,
size: 16, size: 16,
color: Colors.grey, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
Text( Text(
": $nodeCount", ": $nodeCount",
@@ -1221,10 +1236,10 @@ class _MapScreenState extends State<MapScreen> {
), ),
Row( Row(
children: [ children: [
const Icon( Icon(
Icons.wrong_location, Icons.wrong_location,
size: 16, size: 16,
color: Colors.grey, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
Text( Text(
": ${nodeCountAll - nodeCount}", ": ${nodeCountAll - nodeCount}",
@@ -1237,10 +1252,10 @@ class _MapScreenState extends State<MapScreen> {
), ),
Row( Row(
children: [ children: [
const Icon( Icon(
Icons.add_outlined, Icons.add_outlined,
size: 16, size: 16,
color: Colors.grey, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
Text( Text(
": $nodeCountAll", ": $nodeCountAll",
@@ -1536,10 +1551,72 @@ class _MapScreenState extends State<MapScreen> {
LatLng? guessedPosition, LatLng? guessedPosition,
}) { }) {
final connector = context.read<MeshCoreConnector>(); final connector = context.read<MeshCoreConnector>();
showDialog( showModalBottomSheet(
context: context, context: context,
builder: (dialogContext) => AlertDialog( showDragHandle: true,
title: Row( builder: (sheetContext) {
final actions = <Widget>[];
if (contact.type == advTypeChat) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
final unread = connector.getUnreadCountForContactKey(
contact.publicKeyHex,
);
Navigator.pop(sheetContext);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
contact: contact,
initialUnreadCount: unread,
),
),
);
},
child: Text(context.l10n.contacts_openChat),
),
);
}
if (contact.type == advTypeRepeater) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(sheetContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
);
}
if (contact.type == advTypeRoom) {
actions.add(
FilledButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(sheetContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
);
}
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Icon( Icon(
_getNodeIcon(contact.type), _getNodeIcon(contact.type),
@@ -1549,10 +1626,7 @@ class _MapScreenState extends State<MapScreen> {
Expanded(child: SelectableText(contact.name)), Expanded(child: SelectableText(contact.name)),
], ],
), ),
content: Column( const SizedBox(height: 8),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow( _buildInfoRow(
context.l10n.map_type, context.l10n.map_type,
contact.typeLabel(context.l10n), contact.typeLabel(context.l10n),
@@ -1576,60 +1650,17 @@ class _MapScreenState extends State<MapScreen> {
_formatLastSeen(contact.lastSeen), _formatLastSeen(contact.lastSeen),
), ),
_buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex), _buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
], const SizedBox(height: 16),
), ...actions,
actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(dialogContext), onPressed: () => Navigator.pop(sheetContext),
child: Text(context.l10n.common_close), child: Text(context.l10n.common_close),
), ),
if (contact.type ==
advTypeChat) // Only show chat button for chat nodes
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
final unread = connector.getUnreadCountForContactKey(
contact.publicKeyHex,
);
Navigator.pop(dialogContext);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
contact: contact,
initialUnreadCount: unread,
),
),
);
},
child: Text(context.l10n.contacts_openChat),
),
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
], ],
), ),
),
);
},
); );
} }
@@ -1714,6 +1745,9 @@ class _MapScreenState extends State<MapScreen> {
child: Text(context.l10n.common_hide), child: Text(context.l10n.common_hide),
), ),
TextButton( TextButton(
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
onPressed: () async { onPressed: () async {
setState(() { setState(() {
_hiddenMarkerIds.add(marker.id); _hiddenMarkerIds.add(marker.id);
@@ -1745,7 +1779,7 @@ class _MapScreenState extends State<MapScreen> {
label, label,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -1810,9 +1844,8 @@ class _MapScreenState extends State<MapScreen> {
); );
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!mounted) return; if (!mounted) return;
showDismissibleSnackBar( messenger.showSnackBar(
messenger.context, SnackBar(content: Text(successMsg)),
content: Text(successMsg),
); );
}, },
), ),
@@ -2202,7 +2235,7 @@ class _MapScreenState extends State<MapScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
_getTimeFilterLabel(settings.mapTimeFilterHours), _getTimeFilterLabel(settings.mapTimeFilterHours),
style: TextStyle(fontSize: 14, color: Colors.grey[700]), style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
Slider( Slider(
value: _hoursToSliderValue(settings.mapTimeFilterHours), value: _hoursToSliderValue(settings.mapTimeFilterHours),
@@ -2354,7 +2387,7 @@ class _MapScreenState extends State<MapScreen> {
if (_pathTrace.isNotEmpty) if (_pathTrace.isNotEmpty)
Text( Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", "${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( SelectableText(
_pathTrace _pathTrace
+17 -73
View File
@@ -9,7 +9,8 @@ import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/empty_state.dart';
import '../widgets/routing_sheet.dart';
import '../widgets/snr_indicator.dart'; import '../widgets/snr_indicator.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
@@ -167,7 +168,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.neighbors_receivedData), content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green, backgroundColor: Theme.of(context).colorScheme.tertiary,
); );
_statusTimeout?.cancel(); _statusTimeout?.cancel();
if (!mounted) return; if (!mounted) return;
@@ -227,7 +228,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.neighbors_requestTimedOut), content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
_recordStatusResult(false); _recordStatusResult(false);
}); });
@@ -241,7 +242,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.neighbors_errorLoading(e.toString())), content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
} }
@@ -279,7 +280,9 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
children: [ children: [
Text( Text(
l10n.neighbors_repeatersNeighbors, l10n.neighbors_repeatersNeighbors,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
Text( Text(
repeater.name, repeater.name,
@@ -287,75 +290,18 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
centerTitle: false, centerTitle: false,
actions: [ actions: [
PopupMenuButton<String>( IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route), icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode, 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: () => onPressed: () =>
PathManagementDialog.show(context, contact: repeater), ContactRoutingSheet.show(context, contact: repeater),
), ),
IconButton( IconButton(
icon: _isLoading icon: _isLoading
@@ -380,11 +326,9 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
if (!_isLoaded && if (!_isLoaded &&
!_hasData && !_hasData &&
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) (_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
Center( EmptyState(
child: Text( icon: Icons.wifi_find,
l10n.neighbors_noData, title: l10n.neighbors_noData,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
), ),
if (_isLoaded || if (_isLoaded ||
_hasData && _hasData &&
@@ -435,7 +379,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
fmtDuration(entry.value['lastHeard'] + 0.0), fmtDuration(entry.value['lastHeard'] + 0.0),
), ),
entry.value['snr'], entry.value['snr'],
connector.currentSf!, connector.currentSf,
), ),
], ],
), ),
@@ -447,7 +391,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
String label, String label,
String value, String value,
double snr, double snr,
int spreadingFactor, int? spreadingFactor,
) { ) {
final snrUi = snrUiFromSNR(snr, spreadingFactor); final snrUi = snrUiFromSNR(snr, spreadingFactor);
return Padding( return Padding(
+72 -14
View File
@@ -103,6 +103,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
double _pathDistanceMeters = 0.0; double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true; bool _showNodeLabels = true;
Contact? _targetContact; 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) { String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes return pathBytes
@@ -168,17 +171,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
tooltip: 'Zoom in', tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1), onPressed: () => _zoomMapBy(1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
tooltip: 'Zoom out', tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1), onPressed: () => _zoomMapBy(-1),
), ),
IconButton( IconButton(
icon: const Icon(Icons.my_location), icon: const Icon(Icons.my_location),
tooltip: 'Center map', tooltip: context.l10n.map_centerMap,
onPressed: _resetMapView, onPressed: _resetMapView,
), ),
], ],
@@ -228,6 +231,23 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
return traceBytes; 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<void> _doPathTrace() async { Future<void> _doPathTrace() async {
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -236,9 +256,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}); });
} }
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final livePath = _resolveLivePath(connector);
_tracedPath = livePath;
final pathTmp = widget.reversePathAround final pathTmp = widget.reversePathAround
? Uint8List.fromList(widget.path.reversed.toList()) ? Uint8List.fromList(livePath.reversed.toList())
: widget.path; : livePath;
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
@@ -248,7 +272,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
noNotify: !mounted, noNotify: !mounted,
); );
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq( final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000, DateTime.now().millisecondsSinceEpoch ~/ 1000,
0, //flags 0, //flags
@@ -414,13 +437,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final tc = _targetContact!; final tc = _targetContact!;
if (tc.hasLocation) { if (tc.hasLocation) {
targetPos = LatLng(tc.latitude!, tc.longitude!); 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. // Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathAround/reversePathAround), the target-side 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. // sits in the middle of the symmetric sequence; .last is the local side.
final lastHop = widget.reversePathAround final lastHop = widget.reversePathAround
? widget.path.first ? _tracedPath.first
: widget.path.last; : _tracedPath.last;
final peers = connector.allContacts final peers = connector.allContacts
.where( .where(
@@ -593,7 +616,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
!_failed2Loaded) !_failed2Loaded)
Center( Center(
child: Card( child: Card(
color: Colors.white.withValues(alpha: 0.9), color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9),
child: Padding( child: Padding(
padding: EdgeInsets.all(12), padding: EdgeInsets.all(12),
child: Text( child: Text(
@@ -640,9 +663,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add( markers.add(
Marker( Marker(
point: point, point: point,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35, width: 35,
height: 35, height: 35,
child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: hasGps color: hasGps
@@ -669,6 +695,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
), ),
), ),
), ),
),
); );
if (showLabels) { if (showLabels) {
markers.add( markers.add(
@@ -689,9 +716,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add( markers.add(
Marker( Marker(
point: selfPoint, point: selfPoint,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35, width: 35,
height: 35, height: 35,
child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue, color: Colors.blue,
@@ -716,6 +746,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
), ),
), ),
), ),
),
); );
if (showLabels) { if (showLabels) {
markers.add( markers.add(
@@ -735,9 +766,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers.add( markers.add(
Marker( Marker(
point: targetPos, point: targetPos,
width: 48,
height: 48,
child: Center(
child: Container(
width: 35, width: 35,
height: 35, height: 35,
child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isGuessed color: isGuessed
@@ -757,6 +791,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
child: const Icon(Icons.person, color: Colors.white, size: 18), child: const Icon(Icons.person, color: Colors.white, size: 18),
), ),
), ),
),
); );
if (showLabels) { if (showLabels) {
markers.add( markers.add(
@@ -927,6 +962,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
); );
} }
Widget _colorDot(Color color) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
Widget _buildLegendCard( Widget _buildLegendCard(
BuildContext context, BuildContext context,
PathTraceData pathTraceData, PathTraceData pathTraceData,
@@ -949,10 +990,27 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Text( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}', '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600), 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), const Divider(height: 1),
Expanded( Expanded(
+40 -81
View File
@@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../widgets/debug_frame_viewer.dart'; import '../widgets/debug_frame_viewer.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/routing_sheet.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
class RepeaterCliScreen extends StatefulWidget { class RepeaterCliScreen extends StatefulWidget {
@@ -252,97 +252,29 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(l10n.repeater_cliTitle), Text(
l10n.repeater_cliTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text( Text(
repeater.name, repeater.name,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
centerTitle: false, centerTitle: false,
actions: [ actions: [
PopupMenuButton<String>( IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route), icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode, 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: () => onPressed: () =>
PathManagementDialog.show(context, contact: repeater), ContactRoutingSheet.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),
);
}
},
), ),
IconButton( IconButton(
icon: const Icon(Icons.help_outline), icon: const Icon(Icons.help_outline),
@@ -354,6 +286,33 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
tooltip: l10n.repeater_clearHistory, tooltip: l10n.repeater_clearHistory,
onPressed: _commandHistory.isEmpty ? null : _clearHistory, onPressed: _commandHistory.isEmpty ? null : _clearHistory,
), ),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'debug') {
if (_commandController.text.trim().isNotEmpty) {
_sendCommand(showDebug: true);
} else {
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_enterCommandFirst),
);
}
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'debug',
child: Row(
children: [
const Icon(Icons.bug_report),
const SizedBox(width: 8),
Text(l10n.repeater_debugNextCommand),
],
),
),
],
),
], ],
), ),
body: Column( body: Column(
@@ -426,16 +385,16 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
l10n.repeater_noCommandsSent, 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), const SizedBox(height: 8),
Text( Text(
l10n.repeater_typeCommandOrUseQuick, l10n.repeater_typeCommandOrUseQuick,
style: TextStyle(fontSize: 14, color: Colors.grey[500]), style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
], ],
), ),
+14 -14
View File
@@ -72,11 +72,11 @@ class RepeaterHubScreen extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 40, radius: 40,
backgroundColor: Colors.orange, backgroundColor: Theme.of(context).colorScheme.tertiaryContainer,
child: const Icon( child: Icon(
Icons.cell_tower, Icons.cell_tower,
size: 40, size: 40,
color: Colors.white, color: Theme.of(context).colorScheme.onTertiaryContainer,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -90,12 +90,12 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
repeater.shortPubKeyHex, repeater.shortPubKeyHex,
style: TextStyle(fontSize: 14, color: Colors.grey[600]), style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
repeater.pathLabel(context.l10n), 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) ...[ if (repeater.hasLocation) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -105,14 +105,14 @@ class RepeaterHubScreen extends StatelessWidget {
Icon( Icon(
Icons.location_on, Icons.location_on,
size: 14, size: 14,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}', '${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -193,7 +193,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.analytics, icon: Icons.analytics,
title: l10n.repeater_status, title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle, subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue, color: Theme.of(context).colorScheme.primary,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -213,7 +213,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.bar_chart_sharp, icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry, title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle, subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal, color: Theme.of(context).colorScheme.secondary,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -231,7 +231,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.terminal, icon: Icons.terminal,
title: l10n.repeater_cli, title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle, subtitle: l10n.repeater_cliSubtitle,
color: Colors.green, color: Theme.of(context).colorScheme.tertiary,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -251,7 +251,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.group, icon: Icons.group,
title: l10n.repeater_neighbors, title: l10n.repeater_neighbors,
subtitle: l10n.repeater_neighborsSubtitle, subtitle: l10n.repeater_neighborsSubtitle,
color: Colors.orange, color: Theme.of(context).colorScheme.tertiary,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -270,7 +270,7 @@ class RepeaterHubScreen extends StatelessWidget {
icon: Icons.settings, icon: Icons.settings,
title: l10n.repeater_settings, title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle, subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange, color: Theme.of(context).colorScheme.error,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -329,12 +329,12 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
subtitle, 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),
], ],
), ),
), ),
+46 -79
View File
@@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/routing_sheet.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
class RepeaterSettingsScreen extends StatefulWidget { class RepeaterSettingsScreen extends StatefulWidget {
@@ -126,6 +126,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// Location settings // Location settings
final TextEditingController _latController = TextEditingController(); final TextEditingController _latController = TextEditingController();
final TextEditingController _lonController = TextEditingController(); final TextEditingController _lonController = TextEditingController();
bool _latInvalid = false;
bool _lonInvalid = false;
// Feature toggles // Feature toggles
bool _repeatEnabled = true; bool _repeatEnabled = true;
@@ -457,7 +459,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
? l10n.repeater_refreshed(label) ? l10n.repeater_refreshed(label)
: l10n.repeater_errorRefreshing(label), : l10n.repeater_errorRefreshing(label),
), ),
backgroundColor: successCount > 0 ? Colors.green : Colors.red, backgroundColor: successCount > 0 ? null : Theme.of(context).colorScheme.error,
); );
setState(() => setRefreshing(false)); setState(() => setRefreshing(false));
} }
@@ -667,15 +669,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
: l10n.repeater_actionSucceeded(label), : l10n.repeater_actionSucceeded(label),
), ),
backgroundColor: outcome == _SaveOutcome.error backgroundColor: outcome == _SaveOutcome.error
? Colors.red ? Theme.of(context).colorScheme.error
: Colors.green, : null,
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(l10n.repeater_actionFailed(label, e.toString())), content: Text(l10n.repeater_actionFailed(label, e.toString())),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} finally { } finally {
if (mounted) setState(() => _runningAction = false); if (mounted) setState(() => _runningAction = false);
@@ -768,14 +770,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
} }
if (_dirtyFields.contains(_SettingField.lat) && if (_dirtyFields.contains(_SettingField.lat) &&
_latController.text.isNotEmpty) { _latController.text.isNotEmpty &&
_isValidCoordinate(_latController.text, 90)) {
pending.add(( pending.add((
field: _SettingField.lat, field: _SettingField.lat,
command: 'set lat ${_latController.text}', command: 'set lat ${_latController.text}',
)); ));
} }
if (_dirtyFields.contains(_SettingField.lon) && if (_dirtyFields.contains(_SettingField.lon) &&
_lonController.text.isNotEmpty) { _lonController.text.isNotEmpty &&
_isValidCoordinate(_lonController.text, 180)) {
pending.add(( pending.add((
field: _SettingField.lon, field: _SettingField.lon,
command: 'set lon ${_lonController.text}', command: 'set lon ${_lonController.text}',
@@ -944,13 +948,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(l10n.repeater_settingsSavedRebootNeeded), content: Text(l10n.repeater_settingsSavedRebootNeeded),
backgroundColor: Colors.orange, backgroundColor: Theme.of(context).colorScheme.tertiary,
); );
} else if (failures.isEmpty) { } else if (failures.isEmpty) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(l10n.repeater_settingsSaved), content: Text(l10n.repeater_settingsSaved),
backgroundColor: Colors.green,
); );
} else { } else {
showDismissibleSnackBar( showDismissibleSnackBar(
@@ -958,7 +961,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
content: Text( content: Text(
l10n.repeater_settingsPartialFailure(failures.join('; ')), l10n.repeater_settingsPartialFailure(failures.join('; ')),
), ),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
} }
@@ -973,7 +976,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
content: Text( content: Text(
context.l10n.repeater_errorSavingSettings(e.toString()), context.l10n.repeater_errorSavingSettings(e.toString()),
), ),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
} }
@@ -984,6 +987,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_flagHasChanges(); _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() { void _flagHasChanges() {
if (!_hasChanges) { if (!_hasChanges) {
setState(() { setState(() {
@@ -1072,73 +1081,11 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
), ),
centerTitle: false, centerTitle: false,
actions: [ actions: [
PopupMenuButton<String>( IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route), icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode, 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: () => onPressed: () =>
PathManagementDialog.show(context, contact: repeater), ContactRoutingSheet.show(context, contact: repeater),
), ),
if (_hasChanges) if (_hasChanges)
TextButton.icon( TextButton.icon(
@@ -1173,6 +1120,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildAdvancedCard(), _buildAdvancedCard(),
const SizedBox(height: 32), const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 16),
_buildDangerZoneCard(), _buildDangerZoneCard(),
], ],
), ),
@@ -1388,13 +1337,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.repeater_latitude, labelText: l10n.repeater_latitude,
helperText: l10n.repeater_latitudeHelper, helperText: l10n.repeater_latitudeHelper,
errorText: _latInvalid
? l10n.settings_locationInvalid
: null,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, decimal: true,
signed: 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), const SizedBox(width: 8),
@@ -1415,13 +1373,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.repeater_longitude, labelText: l10n.repeater_longitude,
helperText: l10n.repeater_longitudeHelper, helperText: l10n.repeater_longitudeHelper,
errorText: _lonInvalid
? l10n.settings_locationInvalid
: null,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, decimal: true,
signed: 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), const SizedBox(width: 8),
@@ -2233,7 +2200,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(l10n.repeater_errorSendingCommand(e.toString())), content: Text(l10n.repeater_errorSendingCommand(e.toString())),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
} }
@@ -2262,7 +2229,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
onConfirm(); onConfirm();
}, },
style: isDestructive style: isDestructive
? FilledButton.styleFrom(backgroundColor: Colors.red) ? FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error)
: null, : null,
child: Text(l10n.repeater_confirm), child: Text(l10n.repeater_confirm),
), ),
+17 -71
View File
@@ -11,7 +11,7 @@ import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/routing_sheet.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
class RepeaterStatusScreen extends StatefulWidget { class RepeaterStatusScreen extends StatefulWidget {
@@ -318,7 +318,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.repeater_statusRequestTimeout), content: Text(context.l10n.repeater_statusRequestTimeout),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
_recordStatusResult(false); _recordStatusResult(false);
}); });
@@ -331,7 +331,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())), content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
_recordStatusResult(false); _recordStatusResult(false);
@@ -360,82 +360,29 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(l10n.repeater_statusTitle), Text(
l10n.repeater_statusTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text( Text(
repeater.name, repeater.name,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
centerTitle: false, centerTitle: false,
actions: [ actions: [
PopupMenuButton<String>( IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route), icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode, 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: () => onPressed: () =>
PathManagementDialog.show(context, contact: repeater), ContactRoutingSheet.show(context, contact: repeater),
), ),
IconButton( IconButton(
icon: _isLoading icon: _isLoading
@@ -588,21 +535,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Expanded(
width: 130,
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ),
Expanded( const SizedBox(width: 8),
child: Text( Text(
value, value,
style: const TextStyle(fontWeight: FontWeight.w400), style: const TextStyle(fontWeight: FontWeight.w400),
), textAlign: TextAlign.end,
), ),
], ],
), ),
+77 -77
View File
@@ -10,6 +10,7 @@ import '../services/linux_ble_error_classifier.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart'; import '../widgets/device_tile.dart';
import '../widgets/empty_state.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart'; import 'channels_screen.dart';
import 'tcp_screen.dart'; import 'tcp_screen.dart';
@@ -25,6 +26,7 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen> { class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false; bool _changedNavigation = false;
String? _connectingDeviceId;
late final MeshCoreConnector _connector; late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener; late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
@@ -101,6 +103,32 @@ class _ScannerScreenState extends State<ScannerScreen> {
title: AdaptiveAppBarTitle(context.l10n.scanner_title), title: AdaptiveAppBarTitle(context.l10n.scanner_title),
centerTitle: true, centerTitle: true,
automaticallyImplyLeading: false, 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( body: SafeArea(
top: false, top: false,
@@ -122,69 +150,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
}, },
), ),
), ),
bottomNavigationBar: Consumer<MeshCoreConnector>( floatingActionButton: Consumer<MeshCoreConnector>(
builder: (context, connector, child) { builder: (context, connector, child) {
final isScanning = final isScanning =
connector.state == MeshCoreConnectionState.scanning; connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final usbSupported = PlatformInfo.supportsUsbSerial;
final tcpSupported = !PlatformInfo.isWeb;
return SafeArea( return FloatingActionButton.extended(
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', heroTag: 'scanner_ble_action',
onPressed: isBluetoothOff onPressed: isBluetoothOff ? null : () => _toggleScan(connector),
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
appLogger.warn(
'startScan error: $e',
tag: 'ScannerScreen',
);
}),
);
}
},
icon: isScanning icon: isScanning
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
@@ -193,13 +167,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
) )
: const Icon(Icons.bluetooth_searching), : const Icon(Icons.bluetooth_searching),
label: Text( label: Text(
isScanning isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan,
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
),
],
),
), ),
); );
}, },
@@ -207,6 +175,18 @@ class _ScannerScreenState extends State<ScannerScreen> {
); );
} }
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) { Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
String statusText; String statusText;
Color statusColor; Color statusColor;
@@ -254,32 +234,43 @@ class _ScannerScreenState extends State<ScannerScreen> {
Widget _buildDeviceList(BuildContext context, MeshCoreConnector connector) { Widget _buildDeviceList(BuildContext context, MeshCoreConnector connector) {
if (connector.scanResults.isEmpty) { if (connector.scanResults.isEmpty) {
return Center( final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
child: Column( final isScanning = connector.state == MeshCoreConnectionState.scanning;
mainAxisAlignment: MainAxisAlignment.center, return EmptyState(
children: [ icon: isBluetoothOff ? Icons.bluetooth_disabled : Icons.bluetooth,
Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]), title: isBluetoothOff
const SizedBox(height: 16), ? context.l10n.scanner_bluetoothOff
Text( : isScanning
connector.state == MeshCoreConnectionState.scanning
? context.l10n.scanner_searchingDevices ? context.l10n.scanner_searchingDevices
: context.l10n.scanner_tapToScan, : context.l10n.scanner_tapToScan,
style: TextStyle(fontSize: 16, color: Colors.grey[600]), subtitle: isBluetoothOff
), ? context.l10n.scanner_bluetoothOffMessage
], : null,
action: (isBluetoothOff || isScanning)
? null
: FilledButton.icon(
onPressed: () => _toggleScan(connector),
icon: const Icon(Icons.bluetooth_searching),
label: Text(context.l10n.scanner_scan),
), ),
); );
} }
final isConnecting =
connector.state == MeshCoreConnectionState.connecting;
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
itemCount: connector.scanResults.length, itemCount: connector.scanResults.length,
separatorBuilder: (context, index) => const Divider(), separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final result = connector.scanResults[index]; final result = connector.scanResults[index];
final deviceId = result.device.remoteId.toString();
return DeviceTile( return DeviceTile(
scanResult: result, 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<ScannerScreen> {
final name = result.device.platformName.isNotEmpty final name = result.device.platformName.isNotEmpty
? result.device.platformName ? result.device.platformName
: result.advertisementData.advName; : result.advertisementData.advName;
setState(() {
_connectingDeviceId = result.device.remoteId.toString();
});
try { try {
await connector.connect( await connector.connect(
result.device, result.device,
@@ -321,9 +315,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.scanner_connectionFailed(e.toString())), content: Text(context.l10n.scanner_connectionFailed(e.toString())),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
} finally {
if (mounted) {
setState(() {
_connectingDeviceId = null;
});
}
} }
} }
+89 -34
View File
@@ -53,6 +53,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _loadVersionInfo() async { Future<void> _loadVersionInfo() async {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
if (!mounted) return;
setState(() { setState(() {
_appVersion = packageInfo.version; _appVersion = packageInfo.version;
}); });
@@ -213,12 +214,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (percent == null) { if (percent == null) {
icon = Icons.battery_unknown; icon = Icons.battery_unknown;
iconColor = Colors.grey; iconColor = Theme.of(context).colorScheme.onSurfaceVariant;
valueColor = null; valueColor = null;
} else if (percent <= 15) { } else if (percent <= 15) {
icon = Icons.battery_alert; icon = Icons.battery_alert;
iconColor = Colors.orange; iconColor = Theme.of(context).colorScheme.tertiary;
valueColor = Colors.orange; valueColor = Theme.of(context).colorScheme.tertiary;
} else { } else {
icon = Icons.battery_full; icon = Icons.battery_full;
iconColor = null; iconColor = null;
@@ -307,18 +308,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _editLocation(context, connector), 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), const Divider(height: 1),
ListTile( ListTile(
leading: const Icon(Icons.group_add_outlined), leading: const Icon(Icons.group_add_outlined),
@@ -354,13 +343,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
), ),
ListTile( 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), title: Text(l10n.settings_deleteAllPaths),
subtitle: Text( subtitle: Text(
l10n.settings_deleteAllPathsSubtitle, 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), const Divider(height: 1),
ListTile( ListTile(
@@ -378,7 +367,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
const Divider(height: 1), const Divider(height: 1),
ListTile( 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), title: Text(l10n.settings_rebootDevice),
subtitle: Text(l10n.settings_rebootDeviceSubtitle), subtitle: Text(l10n.settings_rebootDeviceSubtitle),
onTap: () => _confirmReboot(context, connector), onTap: () => _confirmReboot(context, connector),
@@ -565,6 +554,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
int.tryParse(customVars["gps_interval"] ?? "") ?? 900; int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString(); intervalController.text = currentInterval.toString();
String? intervalError;
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => StatefulBuilder( builder: (dialogContext) => StatefulBuilder(
@@ -600,9 +591,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: intervalController, controller: intervalController,
onChanged: (_) {
if (intervalError != null) {
setDialogState(() => intervalError = null);
}
},
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.settings_locationIntervalSec, labelText: l10n.settings_locationIntervalSec,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: intervalError,
), ),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: false, decimal: false,
@@ -633,24 +630,25 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
Navigator.pop(context); int? interval;
if (hasGPS) { if (hasGPS) {
final intervalText = intervalController.text.trim(); final intervalText = intervalController.text.trim();
if (intervalText.isEmpty) { if (intervalText.isNotEmpty) {
interval = int.tryParse(intervalText);
if (interval == null ||
interval < 60 ||
interval >= 86400) {
setDialogState(() {
intervalError = l10n.settings_locationIntervalInvalid;
});
return; 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.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
@@ -716,6 +714,36 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
void _confirmDeleteAllPaths(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_deleteAllPaths),
content: Text(l10n.settings_deleteAllPathsSubtitle),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () {
Navigator.pop(context);
connector.deleteAllPaths();
},
child: Text(
l10n.common_deleteAll,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
);
}
void _confirmReboot(BuildContext context, MeshCoreConnector connector) { void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
showDialog( showDialog(
@@ -735,7 +763,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}, },
child: Text( child: Text(
l10n.common_reboot, 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; bool _clientRepeat = false;
int? _selectedPresetIndex; int? _selectedPresetIndex;
_RadioSettingsSnapshot? _lastNonRepeatSnapshot; _RadioSettingsSnapshot? _lastNonRepeatSnapshot;
String? _frequencyError;
String? _txPowerError;
AppDebugLogService get _appLog => AppDebugLogService get _appLog =>
Provider.of<AppDebugLogService>(context, listen: false); Provider.of<AppDebugLogService>(context, listen: false);
@@ -1392,7 +1422,24 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
void _handleManualSettingsChanged(String source) { void _handleManualSettingsChanged(String source) {
_logRadioSettingsState('Manual settings edit: $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) { void _handleClientRepeatChanged(bool enabled) {
@@ -1504,6 +1551,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
content: Text(l10n.settings_error(e.toString())), content: Text(l10n.settings_error(e.toString())),
); );
} }
if (!mounted) return;
Navigator.pop(context); Navigator.pop(context);
} }
@@ -1579,6 +1627,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
labelText: l10n.settings_frequency, labelText: l10n.settings_frequency,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
helperText: l10n.settings_frequencyHelper, helperText: l10n.settings_frequencyHelper,
errorText: _frequencyError,
), ),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, decimal: true,
@@ -1662,6 +1711,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
helperText: widget.connector.maxTxPower != null helperText: widget.connector.maxTxPower != null
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)' ? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
: l10n.settings_txPowerHelper, : l10n.settings_txPowerHelper,
errorText: _txPowerError,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
@@ -1683,7 +1733,12 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel), 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),
),
], ],
); );
} }
+21 -29
View File
@@ -95,12 +95,14 @@ class _TcpScreenState extends State<TcpScreen> {
final isConnecting = final isConnecting =
connector.state == MeshCoreConnectionState.connecting && connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.tcp; connector.activeTransport == MeshCoreTransportType.tcp;
final isButtonDisabled = // A running BLE scan must not block TCP connect: connectTcp() stops
isConnecting || // any active scan before connecting, so the only reason to disable
connector.state == MeshCoreConnectionState.scanning; // the button is a TCP connect already in flight.
final isButtonDisabled = isConnecting;
return Column( return Column(
children: [ children: [
_buildStatusBar(context, connector), _buildStatusBar(context, connector),
_buildTransportLinks(context),
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@@ -154,41 +156,33 @@ class _TcpScreenState extends State<TcpScreen> {
}, },
), ),
), ),
bottomNavigationBar: SafeArea( );
top: false, }
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox( Widget _buildTransportLinks(BuildContext context) {
fit: BoxFit.scaleDown, return Padding(
alignment: Alignment.centerRight, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row( child: Wrap(
mainAxisAlignment: MainAxisAlignment.end, spacing: 12,
runSpacing: 8,
children: [ children: [
if (PlatformInfo.supportsUsbSerial) if (PlatformInfo.supportsUsbSerial)
FloatingActionButton.extended( OutlinedButton.icon(
onPressed: () { onPressed: () {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const UsbScreen()), MaterialPageRoute(builder: (_) => const UsbScreen()),
); );
}, },
heroTag: 'tcp_usb_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
icon: const Icon(Icons.usb), icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel), label: Text(context.l10n.connectionChoiceUsbLabel),
), ),
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12), OutlinedButton.icon(
FloatingActionButton.extended( onPressed: () => Navigator.of(context).maybePop(),
onPressed: () {
Navigator.of(context).maybePop();
},
heroTag: 'tcp_ble_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
icon: const Icon(Icons.bluetooth), icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel), label: Text(context.l10n.connectionChoiceBluetoothLabel),
), ),
], ],
), ),
),
),
); );
} }
@@ -214,7 +208,7 @@ class _TcpScreenState extends State<TcpScreen> {
statusColor = Colors.orange; statusColor = Colors.orange;
} else { } else {
statusText = l10n.tcpStatus_notConnected; statusText = l10n.tcpStatus_notConnected;
statusColor = Colors.grey; statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
} }
return Container( return Container(
@@ -226,18 +220,16 @@ class _TcpScreenState extends State<TcpScreen> {
Icon(Icons.circle, size: 12, color: statusColor), Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text( child: Text(
statusText, statusText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: statusColor, color: statusColor,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ),
),
], ],
), ),
); );
@@ -274,7 +266,7 @@ class _TcpScreenState extends State<TcpScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(message), content: Text(message),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
+18 -75
View File
@@ -13,7 +13,7 @@ import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/routing_sheet.dart';
import '../helpers/cayenne_lpp.dart'; import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
@@ -118,7 +118,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.telemetry_requestTimeout), content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
if (isAutoRefreshRequest && _isAutoRefreshEnabled) { if (isAutoRefreshRequest && _isAutoRefreshEnabled) {
@@ -178,7 +178,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.telemetry_receivedData), content: Text(context.l10n.telemetry_receivedData),
backgroundColor: Colors.green,
); );
} }
_statusTimeout?.cancel(); _statusTimeout?.cancel();
@@ -235,7 +234,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.telemetry_errorLoading(e.toString())), 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<TelemetryScreen> {
final connector = context.watch<MeshCoreConnector>(); final connector = context.watch<MeshCoreConnector>();
final settings = context.watch<AppSettingsService>().settings; final settings = context.watch<AppSettingsService>().settings;
final isImperialUnits = settings.unitSystem == UnitSystem.imperial; 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -347,70 +350,11 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
centerTitle: false, centerTitle: false,
bottom: const SyncProgressAppBarBottom(), bottom: const SyncProgressAppBarBottom(),
actions: [ actions: [
PopupMenuButton<String>( IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route), icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode, 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: () => onPressed: () =>
PathManagementDialog.show(context, contact: widget.contact), ContactRoutingSheet.show(context, contact: widget.contact),
), ),
IconButton( IconButton(
icon: _isLoading icon: _isLoading
@@ -441,7 +385,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
Center( Center(
child: Text( child: Text(
l10n.telemetry_noData, l10n.telemetry_noData,
style: const TextStyle(fontSize: 16, color: Colors.grey), style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
), ),
if ((_isLoaded || _hasData) && if ((_isLoaded || _hasData) &&
@@ -718,14 +662,14 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Center(child: Text(l10n.common_disable)), Center(child: Text(l10n.common_disable)),
const Positioned( Positioned(
right: 0, right: 0,
child: SizedBox( child: SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Colors.white, color: Theme.of(context).colorScheme.onPrimary,
), ),
), ),
), ),
@@ -971,21 +915,20 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Expanded(
width: 130,
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ),
Expanded( const SizedBox(width: 8),
child: Text( Text(
value, value,
style: const TextStyle(fontWeight: FontWeight.w400), style: const TextStyle(fontWeight: FontWeight.w400),
), textAlign: TextAlign.end,
), ),
], ],
), ),
+41 -66
View File
@@ -12,7 +12,6 @@ import '../utils/usb_port_labels.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart'; import 'channels_screen.dart';
import 'scanner_screen.dart';
import 'tcp_screen.dart'; import 'tcp_screen.dart';
class UsbScreen extends StatefulWidget { class UsbScreen extends StatefulWidget {
@@ -100,68 +99,25 @@ class _UsbScreenState extends State<UsbScreen> {
return Column( return Column(
children: [ children: [
_buildStatusBar(context, connector), _buildStatusBar(context, connector),
_buildTransportLinks(context),
Expanded(child: _buildPortList(context, connector)), Expanded(child: _buildPortList(context, connector)),
], ],
); );
}, },
), ),
), ),
bottomNavigationBar: Consumer<MeshCoreConnector>( bottomNavigationBar: _supportsHotPlug
builder: (context, connector, child) { ? null
final isLoading = _isLoadingPorts; : SafeArea(
final showBle = true;
final showTcp = !PlatformInfo.isWeb;
return SafeArea(
top: false, top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (showTcp)
FloatingActionButton.extended( FloatingActionButton.extended(
onPressed: () { onPressed: _isLoadingPorts ? null : _loadPorts,
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', heroTag: 'usb_refresh_action',
extendedPadding: const EdgeInsets.symmetric( icon: _isLoadingPorts
horizontal: 12,
),
icon: isLoading
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
@@ -174,7 +130,31 @@ class _UsbScreenState extends State<UsbScreen> {
), ),
), ),
); );
}
Widget _buildTransportLinks(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Wrap(
spacing: 12,
runSpacing: 8,
children: [
if (!PlatformInfo.isWeb)
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const TcpScreen()),
);
}, },
icon: const Icon(Icons.lan),
label: Text(context.l10n.connectionChoiceTcpLabel),
),
OutlinedButton.icon(
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
],
), ),
); );
} }
@@ -186,7 +166,7 @@ class _UsbScreenState extends State<UsbScreen> {
if (_isLoadingPorts) { if (_isLoadingPorts) {
statusText = l10n.usbStatus_searching; statusText = l10n.usbStatus_searching;
statusColor = Colors.blue; statusColor = Theme.of(context).colorScheme.primary;
} else if (connector.isUsbTransportConnected) { } else if (connector.isUsbTransportConnected) {
switch (connector.state) { switch (connector.state) {
case MeshCoreConnectionState.connected: case MeshCoreConnectionState.connected:
@@ -199,7 +179,7 @@ class _UsbScreenState extends State<UsbScreen> {
statusColor = Colors.orange; statusColor = Colors.orange;
default: default:
statusText = l10n.usbStatus_notConnected; statusText = l10n.usbStatus_notConnected;
statusColor = Colors.grey; statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
} }
} else if (connector.state == MeshCoreConnectionState.connecting && } else if (connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.usb) { connector.activeTransport == MeshCoreTransportType.usb) {
@@ -207,7 +187,7 @@ class _UsbScreenState extends State<UsbScreen> {
statusColor = Colors.orange; statusColor = Colors.orange;
} else { } else {
statusText = l10n.usbStatus_notConnected; statusText = l10n.usbStatus_notConnected;
statusColor = Colors.grey; statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
} }
return Container( return Container(
@@ -219,18 +199,16 @@ class _UsbScreenState extends State<UsbScreen> {
Icon(Icons.circle, size: 12, color: statusColor), Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text( child: Text(
statusText, statusText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: statusColor, color: statusColor,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ),
),
], ],
), ),
); );
@@ -244,11 +222,11 @@ class _UsbScreenState extends State<UsbScreen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
l10n.usbStatus_searching, 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<UsbScreen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
l10n.usbScreenEmptyState, l10n.usbScreenEmptyState,
textAlign: TextAlign.center, 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<UsbScreen> {
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),
), ),
subtitle: showRawName ? Text(rawName) : null, subtitle: showRawName ? Text(rawName) : null,
trailing: ElevatedButton( trailing: const Icon(Icons.chevron_right),
onPressed: isConnecting ? null : () => _connectPort(port),
child: Text(l10n.common_connect),
),
onTap: isConnecting ? null : () => _connectPort(port), onTap: isConnecting ? null : () => _connectPort(port),
); );
}, },
@@ -387,7 +362,7 @@ class _UsbScreenState extends State<UsbScreen> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(_friendlyErrorMessage(error)), content: Text(_friendlyErrorMessage(error)),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
+5
View File
@@ -449,6 +449,11 @@ class MessageRetryService extends ChangeNotifier {
}); });
} }
void untrack(String messageId) {
_timeoutTimers[messageId]?.cancel();
_cleanupMessage(messageId);
}
void _cleanupMessage(String messageId) { void _cleanupMessage(String messageId) {
_moveAckHashesToHistory(messageId); _moveAckHashesToHistory(messageId);
_ackHashToMessageId.removeWhere( _ackHashToMessageId.removeWhere(
+3 -2
View File
@@ -248,7 +248,7 @@ class NotificationService {
await _notifications.show( await _notifications.show(
id: contactId != null id: contactId != null
? 'advert:$contactId'.hashCode ? 'advert:$contactId'.hashCode
: DateTime.now().millisecondsSinceEpoch, : DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF,
title: _l10n.notification_newTypeDiscovered(contactType), title: _l10n.notification_newTypeDiscovered(contactType),
body: contactName, body: contactName,
notificationDetails: notificationDetails, notificationDetails: notificationDetails,
@@ -304,7 +304,8 @@ class NotificationService {
try { try {
await _notifications.show( await _notifications.show(
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, id: channelIndex?.hashCode ??
DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF,
title: channelName, title: channelName,
body: body, body: body,
notificationDetails: notificationDetails, notificationDetails: notificationDetails,
+22 -9
View File
@@ -47,6 +47,7 @@ class TranslationService extends ChangeNotifier {
_langDetectInit = initLangDetect(); _langDetectInit = initLangDetect();
} }
bool _disposed = false;
bool _isBusy = false; bool _isBusy = false;
bool _isDownloading = false; bool _isDownloading = false;
bool _cancelDownloadRequested = false; bool _cancelDownloadRequested = false;
@@ -215,7 +216,7 @@ class TranslationService extends ChangeNotifier {
} }
_downloadTotalBytes = totalSize; _downloadTotalBytes = totalSize;
notifyListeners(); _notify();
DownloadedModelFile downloaded; DownloadedModelFile downloaded;
if (supportsRange && if (supportsRange &&
@@ -268,7 +269,7 @@ class TranslationService extends ChangeNotifier {
throw StateError('Model download failed: HTTP ${response.statusCode}'); throw StateError('Model download failed: HTTP ${response.statusCode}');
} }
_downloadTotalBytes ??= response.contentLength; _downloadTotalBytes ??= response.contentLength;
notifyListeners(); _notify();
final trackedStream = _trackDownloadProgress(response.stream); final trackedStream = _trackDownloadProgress(response.stream);
return await _fileStore.writeModelBytes( return await _fileStore.writeModelBytes(
fileName: fileName, fileName: fileName,
@@ -313,7 +314,7 @@ class TranslationService extends ChangeNotifier {
throw const TranslationDownloadCancelled(); throw const TranslationDownloadCancelled();
} }
_downloadFileName = 'Merging chunks...'; _downloadFileName = 'Merging chunks...';
notifyListeners(); _notify();
combineReached = true; combineReached = true;
return await _fileStore.combineChunks( return await _fileStore.combineChunks(
fileName: fileName, fileName: fileName,
@@ -361,7 +362,7 @@ class TranslationService extends ChangeNotifier {
} }
_cancelDownloadRequested = true; _cancelDownloadRequested = true;
_lastError = 'Download stopped.'; _lastError = 'Download stopped.';
notifyListeners(); _notify();
} }
Future<void> removeModel(TranslationModelRecord model) async { Future<void> removeModel(TranslationModelRecord model) async {
@@ -469,7 +470,7 @@ class TranslationService extends ChangeNotifier {
} catch (error) { } catch (error) {
_lastError = error.toString(); _lastError = error.toString();
appLogger.warn('Language detection failed: $error'); appLogger.warn('Language detection failed: $error');
notifyListeners(); _notify();
return null; return null;
} }
} }
@@ -538,7 +539,7 @@ class TranslationService extends ChangeNotifier {
} catch (error) { } catch (error) {
_lastError = error.toString(); _lastError = error.toString();
appLogger.warn('Translation request failed: $error'); appLogger.warn('Translation request failed: $error');
notifyListeners(); _notify();
return null; return null;
} }
} }
@@ -631,6 +632,10 @@ class TranslationService extends ChangeNotifier {
final completer = Completer<T>(); final completer = Completer<T>();
_setBusy(true); _setBusy(true);
_queue = _queue.then((_) async { _queue = _queue.then((_) async {
if (_disposed) {
completer.completeError(StateError('TranslationService disposed.'));
return;
}
try { try {
completer.complete(await action()); completer.complete(await action());
} catch (error, stackTrace) { } catch (error, stackTrace) {
@@ -648,17 +653,24 @@ class TranslationService extends ChangeNotifier {
throw const TranslationDownloadCancelled(); throw const TranslationDownloadCancelled();
} }
_downloadedBytes += chunk.length; _downloadedBytes += chunk.length;
notifyListeners(); _notify();
yield chunk; yield chunk;
} }
} }
void _notify() {
if (_disposed) {
return;
}
notifyListeners();
}
void _setBusy(bool value) { void _setBusy(bool value) {
if (_isBusy == value) { if (_isBusy == value) {
return; return;
} }
_isBusy = value; _isBusy = value;
notifyListeners(); _notify();
} }
void _setDownloading(bool value) { void _setDownloading(bool value) {
@@ -669,11 +681,12 @@ class TranslationService extends ChangeNotifier {
_downloadTotalBytes = null; _downloadTotalBytes = null;
_downloadFileName = null; _downloadFileName = null;
} }
notifyListeners(); _notify();
} }
@override @override
void dispose() { void dispose() {
_disposed = true;
final engine = _engine; final engine = _engine;
_engine = null; _engine = null;
_loadedModelPath = null; _loadedModelPath = null;
+55 -48
View File
@@ -1,34 +1,30 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// MeshCore redesign palette warm field-journal dark theme with /// MeshCore palette cool slate dark theme with sky-blue accents.
/// phosphor-green signal accents. Mirrors values from the redesign spec.
class MeshPalette { class MeshPalette {
MeshPalette._(); MeshPalette._();
// Surfaces (warm near-black, olive undertone) // Surfaces (cool near-black, slate undertone)
static const bg = Color(0xFF0F1412); static const bg = Color(0xFF101417);
static const bg1 = Color(0xFF161C19); static const bg1 = Color(0xFF161B1F);
static const bg2 = Color(0xFF1D2521); static const bg2 = Color(0xFF1D242A);
static const bg3 = Color(0xFF28322D); static const bg3 = Color(0xFF28313A);
static const bg4 = Color(0xFF34403A); static const bg4 = Color(0xFF344049);
// Lines // Lines
static const line = Color(0xFF232C28); static const line = Color(0xFF222B31);
static const line2 = Color(0xFF34403A); static const line2 = Color(0xFF344049);
static const line3 = Color(0xFF48564F); static const line3 = Color(0xFF485762);
// Ink // Ink
static const ink = Color(0xFFEFF3E8); static const ink = Color(0xFFE9EEF3);
static const ink2 = Color(0xFFBAC4B5); static const ink2 = Color(0xFFB5C0C9);
static const ink3 = Color(0xFF7C8B82); static const ink3 = Color(0xFF7C8A95);
static const ink4 = Color(0xFF55635B); 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 signal = Color(0xFF7BEFA8);
static const signalDim = Color(0xFF4DC580); 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) // Warn (ember)
static const warn = Color(0xFFFFA552); static const warn = Color(0xFFFFA552);
@@ -41,8 +37,9 @@ class MeshPalette {
static const alertBg = Color(0x1CFF6A5C); static const alertBg = Color(0x1CFF6A5C);
static const alertLine = Color(0x52FF6A5C); static const alertLine = Color(0x52FF6A5C);
// Blue (dusk sky) // Blue (sky) primary accent
static const blue = Color(0xFF7FCBF5); static const blue = Color(0xFF7FCBF5);
static const blueDim = Color(0xFF4A9CC9);
static const blueBg = Color(0x1C7FCBF5); static const blueBg = Color(0x1C7FCBF5);
static const blueLine = Color(0x477FCBF5); static const blueLine = Color(0x477FCBF5);
@@ -51,20 +48,20 @@ class MeshPalette {
static const magentaBg = Color(0x1CDE7FDB); static const magentaBg = Color(0x1CDE7FDB);
static const magentaLine = Color(0x47DE7FDB); static const magentaLine = Color(0x47DE7FDB);
// Me bubble (mossy) // Me bubble (dusk blue)
static const me = Color(0xFF1E3527); static const me = Color(0xFF1B2C3D);
static const meBorder = Color(0xFF2D5039); static const meBorder = Color(0xFF2C4A66);
static const meInk = Color(0xFFDEF0DC); static const meInk = Color(0xFFDCE9F5);
// Light variant (used when user explicitly picks light theme) // Light variant (used when user explicitly picks light theme)
static const lightBg = Color(0xFFF5F3EC); static const lightBg = Color(0xFFF4F6F8);
static const lightBg1 = Color(0xFFECE9DF); static const lightBg1 = Color(0xFFEAEEF2);
static const lightBg2 = Color(0xFFE2DED2); static const lightBg2 = Color(0xFFDFE5EA);
static const lightLine = Color(0xFFCAC5B4); static const lightLine = Color(0xFFC3CCD4);
static const lightInk = Color(0xFF0F1410); static const lightInk = Color(0xFF10161B);
static const lightInk2 = Color(0xFF3D463E); static const lightInk2 = Color(0xFF3C4853);
static const lightInk3 = Color(0xFF6A756D); static const lightInk3 = Color(0xFF69767F);
static const lightSignal = Color(0xFF1A7A44); static const lightBlue = Color(0xFF2F6EA8);
} }
/// Named font stacks Flutter falls back to system fonts when the named /// Named font stacks Flutter falls back to system fonts when the named
@@ -115,14 +112,18 @@ class MeshTheme {
static ThemeData dark() { static ThemeData dark() {
const scheme = ColorScheme.dark( const scheme = ColorScheme.dark(
primary: MeshPalette.signal, primary: MeshPalette.blue,
onPrimary: Color(0xFF0A1810), onPrimary: Color(0xFF0A1A26),
primaryContainer: MeshPalette.signalBg, primaryContainer: MeshPalette.blueBg,
onPrimaryContainer: MeshPalette.signal, onPrimaryContainer: MeshPalette.blue,
secondary: MeshPalette.blue, secondary: MeshPalette.magenta,
onSecondary: Color(0xFF0A1520), onSecondary: Color(0xFF201020),
tertiary: MeshPalette.magenta, secondaryContainer: Color(0xFF331A33),
onTertiary: Color(0xFF201020), onSecondaryContainer: MeshPalette.magenta,
tertiary: MeshPalette.warn,
onTertiary: Color(0xFF1F1206),
tertiaryContainer: Color(0xFF3A2710),
onTertiaryContainer: Color(0xFFFFC58A),
error: MeshPalette.alert, error: MeshPalette.alert,
onError: Color(0xFF1A0A08), onError: Color(0xFF1A0A08),
errorContainer: MeshPalette.alertBg, errorContainer: MeshPalette.alertBg,
@@ -141,33 +142,39 @@ class MeshTheme {
scrim: Colors.black54, scrim: Colors.black54,
inverseSurface: MeshPalette.ink, inverseSurface: MeshPalette.ink,
onInverseSurface: MeshPalette.bg, onInverseSurface: MeshPalette.bg,
inversePrimary: MeshPalette.signalDim, inversePrimary: MeshPalette.blueDim,
); );
return _build(scheme, Brightness.dark); return _build(scheme, Brightness.dark);
} }
static ThemeData light() { static ThemeData light() {
const scheme = ColorScheme.light( const scheme = ColorScheme.light(
primary: MeshPalette.lightSignal, primary: MeshPalette.lightBlue,
onPrimary: Colors.white, onPrimary: Colors.white,
primaryContainer: Color(0xFFD4E8D8), primaryContainer: Color(0xFFD3E4F5),
onPrimaryContainer: MeshPalette.lightSignal, onPrimaryContainer: Color(0xFF12354F),
secondary: Color(0xFF2F6EA8), secondary: Color(0xFF8C4A8A),
onSecondary: Colors.white, onSecondary: Colors.white,
tertiary: Color(0xFF8C4A8A), secondaryContainer: Color(0xFFEFD6EE),
onSecondaryContainer: Color(0xFF3D1A3C),
tertiary: Color(0xFF9A5B16),
onTertiary: Colors.white, onTertiary: Colors.white,
tertiaryContainer: Color(0xFFF8E3C9),
onTertiaryContainer: Color(0xFF4A2A05),
error: Color(0xFFB53D2F), error: Color(0xFFB53D2F),
onError: Colors.white, onError: Colors.white,
errorContainer: Color(0xFFF6D9D4),
onErrorContainer: Color(0xFF5C1A12),
surface: MeshPalette.lightBg, surface: MeshPalette.lightBg,
onSurface: MeshPalette.lightInk, onSurface: MeshPalette.lightInk,
surfaceContainerLowest: MeshPalette.lightBg, surfaceContainerLowest: MeshPalette.lightBg,
surfaceContainerLow: MeshPalette.lightBg1, surfaceContainerLow: MeshPalette.lightBg1,
surfaceContainer: MeshPalette.lightBg1, surfaceContainer: MeshPalette.lightBg1,
surfaceContainerHigh: MeshPalette.lightBg2, surfaceContainerHigh: MeshPalette.lightBg2,
surfaceContainerHighest: Color(0xFFD5D0C0), surfaceContainerHighest: Color(0xFFD2DAE1),
onSurfaceVariant: MeshPalette.lightInk2, onSurfaceVariant: MeshPalette.lightInk2,
outline: MeshPalette.lightLine, outline: MeshPalette.lightLine,
outlineVariant: Color(0xFFDBD6C6), outlineVariant: Color(0xFFD8DEE5),
); );
return _build(scheme, Brightness.light); return _build(scheme, Brightness.light);
} }
+5 -3
View File
@@ -86,7 +86,7 @@ class ByteCountedTextField extends StatelessWidget {
final counterColor = ratio > errorThreshold final counterColor = ratio > errorThreshold
? Theme.of(context).colorScheme.error ? Theme.of(context).colorScheme.error
: ratio > warningThreshold : ratio > warningThreshold
? Colors.orange ? Theme.of(context).colorScheme.tertiary
: Theme.of(context).colorScheme.onSurfaceVariant; : Theme.of(context).colorScheme.onSurfaceVariant;
return Column( return Column(
@@ -118,8 +118,9 @@ class ByteCountedTextField extends StatelessWidget {
textInputAction: textInputAction, textInputAction: textInputAction,
onSubmitted: onSubmitted, onSubmitted: onSubmitted,
), ),
if (showCounter) Opacity(
Padding( opacity: showCounter ? 1 : 0,
child: Padding(
padding: const EdgeInsets.only(top: 4, right: 4), padding: const EdgeInsets.only(top: 4, right: 4),
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@@ -129,6 +130,7 @@ class ByteCountedTextField extends StatelessWidget {
), ),
), ),
), ),
),
], ],
); );
}, },
+16 -6
View File
@@ -6,9 +6,15 @@ import 'signal_ui.dart';
/// A reusable tile widget for displaying a MeshCore device in a list /// A reusable tile widget for displaying a MeshCore device in a list
class DeviceTile extends StatelessWidget { class DeviceTile extends StatelessWidget {
final ScanResult scanResult; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -19,16 +25,20 @@ class DeviceTile extends StatelessWidget {
: scanResult.advertisementData.advName; : scanResult.advertisementData.advName;
return ListTile( return ListTile(
enabled: onTap != null || isConnecting,
leading: _buildSignalIcon(rssi), leading: _buildSignalIcon(rssi),
title: Text( title: Text(
name.isNotEmpty ? name : context.l10n.common_unknownDevice, name.isNotEmpty ? name : context.l10n.common_unknownDevice,
style: const TextStyle(fontWeight: FontWeight.w500), style: const TextStyle(fontWeight: FontWeight.w500),
), ),
subtitle: Text(device.remoteId.toString()), subtitle: Text(device.remoteId.toString()),
trailing: ElevatedButton( trailing: isConnecting
onPressed: onTap, ? const SizedBox(
child: Text(context.l10n.common_connect), width: 20,
), height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
onTap: onTap, onTap: onTap,
); );
} }
+7 -3
View File
@@ -17,18 +17,22 @@ class EmptyState extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final onSurfaceVariant = Theme.of(context).colorScheme.onSurfaceVariant;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(icon, size: 64, color: Colors.grey[400]), Icon(icon, size: 64, color: onSurfaceVariant.withValues(alpha: 0.6)),
const SizedBox(height: 16), 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) ...[ if (subtitle != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
subtitle!, subtitle!,
style: TextStyle(fontSize: 14, color: Colors.grey[500]), style: TextStyle(
fontSize: 14,
color: onSurfaceVariant.withValues(alpha: 0.8),
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
+5 -5
View File
@@ -180,7 +180,7 @@ class _GifPickerState extends State<GifPicker> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
context.l10n.gifPicker_poweredBy, 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<GifPicker> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
_error!, _error!,
style: TextStyle(fontSize: 16, color: Colors.grey[600]), style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
@@ -219,11 +219,11 @@ class _GifPickerState extends State<GifPicker> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 16),
Text( Text(
context.l10n.gifPicker_noGifsFound, context.l10n.gifPicker_noGifsFound,
style: TextStyle(fontSize: 16, color: Colors.grey[600]), style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
], ],
), ),
+128 -9
View File
@@ -1,36 +1,155 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.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 isAcked;
final bool isFailed; final bool isFailed;
final bool isPending;
final bool isRepeated;
final double size; 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({ const MessageStatusIcon({
super.key, super.key,
required this.isAcked, required this.isAcked,
this.isFailed = false, this.isFailed = false,
this.isPending = false,
this.isRepeated = false,
this.size = 14, this.size = 14,
this.onColor,
}); });
@override
State<MessageStatusIcon> createState() => _MessageStatusIconState();
}
class _MessageStatusIconState extends State<MessageStatusIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
if (widget.isPending) _controller.repeat();
}
@override
void didUpdateWidget(MessageStatusIcon oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isPending && !_controller.isAnimating) {
_controller.repeat();
} else if (!widget.isPending && _controller.isAnimating) {
_controller
..stop()
..reset();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isFailed) { final l10n = context.l10n;
return Icon(Icons.cancel, size: size, color: Colors.red); final colorScheme = Theme.of(context).colorScheme;
final double size = widget.size;
final Color baseColor = widget.onColor ?? colorScheme.onSurfaceVariant;
if (widget.isFailed) {
return Semantics(
label: l10n.messageStatus_failed,
child: Icon(Icons.cancel, size: size, color: colorScheme.error),
);
} }
final Color color; if (widget.isPending) {
if (isAcked) { return Semantics(
color = Colors.green; label: l10n.messageStatus_pending,
} else { child: _SendingDots(
color = Colors.grey; controller: _controller,
color: baseColor,
size: size,
),
);
} }
return SvgPicture.asset( final bool delivered = widget.isAcked || widget.isRepeated;
final String label = widget.isRepeated
? l10n.messageStatus_repeated
: widget.isAcked
? l10n.messageStatus_delivered
: l10n.messageStatus_sent;
final Color color = delivered ? colorScheme.tertiary : baseColor;
return Semantics(
label: label,
child: delivered
? SvgPicture.asset(
'assets/icons/done_all.svg', 'assets/icons/done_all.svg',
width: size, width: size,
height: size, height: size,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn), colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
)
: Icon(Icons.done, size: size, color: color),
);
}
}
/// Three dots that pulse left-to-right while a message is in flight.
class _SendingDots extends StatelessWidget {
final AnimationController controller;
final Color color;
final double size;
const _SendingDots({
required this.controller,
required this.color,
required this.size,
});
@override
Widget build(BuildContext context) {
final double dot = (size * 0.24).clamp(2.0, 4.0);
return SizedBox(
height: size,
child: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: List.generate(3, (i) {
final double phase = (controller.value - i * 0.18) % 1.0;
final double t = phase < 0.5 ? phase * 2 : (1 - phase) * 2;
final double opacity = 0.25 + 0.75 * t.clamp(0.0, 1.0);
return Padding(
padding: EdgeInsets.symmetric(horizontal: dot * 0.28),
child: Container(
width: dot,
height: dot,
decoration: BoxDecoration(
color: color.withValues(alpha: opacity),
shape: BoxShape.circle,
),
),
);
}),
);
},
),
); );
} }
} }
+377
View File
@@ -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<Contact> availableContacts;
final List<int> initialPath;
const PathEditorSheet({
super.key,
required this.availableContacts,
this.initialPath = const [],
});
static Future<Uint8List?> show(
BuildContext context, {
required List<Contact> availableContacts,
List<int> initialPath = const [],
}) {
return showModalBottomSheet<Uint8List>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: FractionallySizedBox(
heightFactor: 0.9,
child: PathEditorSheet(
availableContacts: availableContacts,
initialPath: initialPath,
),
),
),
);
}
@override
State<PathEditorSheet> createState() => _PathEditorSheetState();
}
class _Hop {
final int id;
final int byte;
const _Hop(this.id, this.byte);
}
class _PathEditorSheetState extends State<PathEditorSheet> {
static const int _maxHops = 64;
final List<_Hop> _hops = [];
final _hexController = TextEditingController();
String? _hexError;
bool _syncingHex = false;
String _search = '';
int _nextHopId = 0;
@override
void initState() {
super.initState();
for (final byte in widget.initialPath) {
_hops.add(_Hop(_nextHopId++, byte));
}
_syncHexFromHops();
}
@override
void dispose() {
_hexController.dispose();
super.dispose();
}
List<Contact> get _repeaters {
final query = _search.trim().toLowerCase();
return widget.availableContacts
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
.where((c) => c.publicKey.isNotEmpty)
.where((c) => query.isEmpty || c.name.toLowerCase().contains(query))
.toList()
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
}
void _syncHexFromHops() {
_syncingHex = true;
_hexController.text = 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),
),
),
],
),
),
),
],
);
}
}
-510
View File
@@ -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<void> show(BuildContext context, {required Contact contact}) {
return showDialog<void>(
context: context,
builder: (context) => _PathManagementDialog(contact: contact),
);
}
}
class _PathManagementDialog extends StatefulWidget {
final Contact contact;
const _PathManagementDialog({required this.contact});
@override
State<_PathManagementDialog> createState() => _PathManagementDialogState();
}
class _PathManagementDialogState extends State<_PathManagementDialog> {
bool _showAllPaths = false;
int _resolveContactIndex = -1;
Contact _resolveContact(MeshCoreConnector connector) {
if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
);
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
}
String _formatRelativeTime(BuildContext context, DateTime? time) {
if (time == null) return '';
final l10n = context.l10n;
final diff = DateTime.now().difference(time);
if (diff.inSeconds < 60) return l10n.time_justNow;
if (diff.inMinutes < 60) return l10n.time_minutesAgo(diff.inMinutes);
if (diff.inHours < 24) return l10n.time_hoursAgo(diff.inHours);
return l10n.time_daysAgo(diff.inDays);
}
void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
final l10n = context.l10n;
if (pathBytes.isEmpty) {
showDismissibleSnackBar(
context,
content: Text(l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2),
);
return;
}
final connector = context.read<MeshCoreConnector>();
final allContacts = connector.allContacts;
final formattedPath = PathHelper.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<void> _setCustomPath(
BuildContext context,
MeshCoreConnector connector,
Contact currentContact,
) async {
final l10n = context.l10n;
if (currentContact.pathLength > 0 &&
currentContact.path.isEmpty &&
connector.isConnected) {
connector.getContacts();
}
final pathForInput = currentContact.pathFormattedIdList(
connector.pathHashByteWidth,
);
final availableContacts = connector.allContacts
.where((c) => c.publicKeyHex != currentContact.publicKeyHex)
.toList();
final result = await PathSelectionDialog.show(
context,
availableContacts: availableContacts,
initialPath: pathForInput.isEmpty ? null : pathForInput,
currentPathLabel: currentContact.pathLabel(l10n),
onRefresh: connector.isConnected ? connector.getContacts : null,
);
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<MeshCoreConnector, PathHistoryService>(
builder: (context, connector, pathService, _) {
final currentContact = _resolveContact(connector);
final paths = pathService.getRecentPaths(currentContact.publicKeyHex);
final repeatersList = List.of(connector.directRepeaters)
..sort((a, b) => b.ranking.compareTo(a.ranking));
if (repeatersList.isEmpty) {
_showAllPaths = true;
}
final directRepeater = repeatersList.isEmpty
? null
: repeatersList.first;
final secondDirectRepeater = repeatersList.length < 2
? null
: repeatersList.elementAt(1);
final thirdDirectRepeater = repeatersList.length < 3
? null
: repeatersList.elementAt(2);
List<MapEntry<int, MapEntry<Color, PathRecord>>> pathsWithRepeaters =
paths.map((path) {
final isDirectRepeater =
directRepeater != null &&
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),
),
],
);
},
);
}
}
-346
View File
@@ -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<Contact> 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<PathSelectionDialog> createState() => _PathSelectionDialogState();
static Future<Uint8List?> show(
BuildContext context, {
required List<Contact> availableContacts,
String? title,
String? initialPath,
String? currentPathLabel,
VoidCallback? onRefresh,
}) {
return showDialog<Uint8List?>(
context: context,
builder: (context) => PathSelectionDialog(
availableContacts: availableContacts,
title: title ?? context.l10n.path_enterCustomPath,
initialPath: initialPath,
currentPathLabel: currentPathLabel,
onRefresh: onRefresh,
),
);
}
}
class _PathSelectionDialogState extends State<PathSelectionDialog> {
late TextEditingController _controller;
final List<Contact> _selectedContacts = [];
List<Contact> _validContacts = [];
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialPath ?? '');
_filterValidContacts();
}
@override
void didUpdateWidget(PathSelectionDialog oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.availableContacts != oldWidget.availableContacts) {
_filterValidContacts();
}
}
void _filterValidContacts() {
_validContacts = widget.availableContacts
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
.toList();
}
void _updateTextFromContacts() {
final 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<void> _validateAndSubmit() async {
final l10n = context.l10n;
final path = _controller.text.trim().toUpperCase();
if (path.isEmpty) {
if (mounted) Navigator.pop(context);
return;
}
// Parse comma-separated hex prefixes
final pathIds = path
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
final pathBytesList = <int>[];
final invalidPrefixes = <String>[];
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),
),
],
);
}
}
+7 -20
View File
@@ -67,10 +67,12 @@ class QuickSwitchBar extends StatelessWidget {
destinations: [ destinations: [
NavigationDestination( NavigationDestination(
icon: _buildIconWithBadge( icon: _buildIconWithBadge(
context,
const Icon(Icons.people_outline), const Icon(Icons.people_outline),
contactsUnreadCount, contactsUnreadCount,
), ),
selectedIcon: _buildIconWithBadge( selectedIcon: _buildIconWithBadge(
context,
const Icon(Icons.people), const Icon(Icons.people),
contactsUnreadCount, contactsUnreadCount,
), ),
@@ -78,10 +80,12 @@ class QuickSwitchBar extends StatelessWidget {
), ),
NavigationDestination( NavigationDestination(
icon: _buildIconWithBadge( icon: _buildIconWithBadge(
context,
const Icon(Icons.tag), const Icon(Icons.tag),
channelsUnreadCount, channelsUnreadCount,
), ),
selectedIcon: _buildIconWithBadge( selectedIcon: _buildIconWithBadge(
context,
const Icon(Icons.tag), const Icon(Icons.tag),
channelsUnreadCount, 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; if (count <= 0) return icon;
final label = count > 99 ? '99+' : '$count';
return Stack( return Badge(label: Text(label), child: icon);
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,
),
),
),
],
);
} }
} }
+5 -1
View File
@@ -59,12 +59,16 @@ class _RadioStatsIconButtonState extends State<RadioStatsIconButton> {
active: connector.radioStatsAirActivityPulse, active: connector.radioStatsAirActivityPulse,
); );
if (widget.compact) { if (widget.compact) {
return GestureDetector( return Semantics(
label: context.l10n.radioStats_tooltip,
button: true,
child: GestureDetector(
onTap: () => pushCompanionRadioStatsScreen(context), onTap: () => pushCompanionRadioStatsScreen(context),
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 4), padding: const EdgeInsets.only(left: 4),
child: dot, child: dot,
), ),
),
); );
} }
return Tooltip( return Tooltip(
+8 -11
View File
@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import '../utils/platform_info.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -11,7 +10,7 @@ import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import 'path_management_dialog.dart'; import 'routing_sheet.dart';
class RepeaterLoginDialog extends StatefulWidget { class RepeaterLoginDialog extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -276,7 +275,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
return AlertDialog( return AlertDialog(
title: Row( title: Row(
children: [ children: [
const Icon(Icons.cell_tower, color: Colors.orange), Icon(Icons.cell_tower, color: Theme.of(context).colorScheme.tertiary),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Column( child: Column(
@@ -288,7 +287,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -365,9 +364,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
} }
}, },
onSubmitted: (_) => _handleLogin(), onSubmitted: (_) => _handleLogin(),
autofocus: autofocus: _passwordController.text.isEmpty,
!PlatformInfo.isMobile &&
_passwordController.text.isEmpty,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
CheckboxListTile( CheckboxListTile(
@@ -469,14 +466,14 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
repeater.pathLabel(context.l10n), 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), const SizedBox(height: 8),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: TextButton.icon( child: TextButton.icon(
onPressed: () => onPressed: () =>
PathManagementDialog.show(context, contact: repeater), ContactRoutingSheet.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18), icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths), label: Text(l10n.login_managePaths),
), ),
@@ -497,12 +494,12 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox( SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Colors.white, color: Theme.of(context).colorScheme.onPrimary,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
+8 -8
View File
@@ -12,7 +12,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
import 'path_management_dialog.dart'; import 'routing_sheet.dart';
class RoomLoginDialog extends StatefulWidget { class RoomLoginDialog extends StatefulWidget {
final Contact room; final Contact room;
@@ -181,7 +181,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.login_failed(e.toString())), content: Text(context.l10n.login_failed(e.toString())),
backgroundColor: Colors.red, backgroundColor: Theme.of(context).colorScheme.error,
); );
} }
} }
@@ -232,7 +232,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
return AlertDialog( return AlertDialog(
title: Row( title: Row(
children: [ children: [
const Icon(Icons.group, color: Colors.purple), Icon(Icons.group, color: Theme.of(context).colorScheme.secondary),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Column( child: Column(
@@ -244,7 +244,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: Colors.grey[600], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -395,14 +395,14 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
repeater.pathLabel(context.l10n), 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), const SizedBox(height: 8),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: TextButton.icon( child: TextButton.icon(
onPressed: () => onPressed: () =>
PathManagementDialog.show(context, contact: repeater), ContactRoutingSheet.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18), icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths), label: Text(l10n.login_managePaths),
), ),
@@ -423,12 +423,12 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox( SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Colors.white, color: Theme.of(context).colorScheme.onPrimary,
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
+709
View File
@@ -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<void> show(BuildContext context, {required Contact contact}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => DraggableScrollableSheet(
expand: false,
initialChildSize: 0.75,
minChildSize: 0.4,
maxChildSize: 0.95,
builder: (context, scrollController) => _RoutingSheetBody(
contact: contact,
scrollController: scrollController,
),
),
);
}
}
class _RoutingSheetBody extends StatefulWidget {
final Contact contact;
final ScrollController scrollController;
const _RoutingSheetBody({
required this.contact,
required this.scrollController,
});
@override
State<_RoutingSheetBody> createState() => _RoutingSheetBodyState();
}
class _RoutingSheetBodyState extends State<_RoutingSheetBody> {
int _resolveContactIndex = -1;
String? _syncStatus;
Contact _resolveContact(MeshCoreConnector connector) {
if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
);
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
}
_RoutingMode _modeOf(Contact contact) {
final override = contact.pathOverride;
if (override == null) return _RoutingMode.auto;
return override < 0 ? _RoutingMode.flood : _RoutingMode.manual;
}
Future<void> _selectMode(
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
) async {
switch (mode) {
case _RoutingMode.auto:
setState(() => _syncStatus = null);
await connector.setPathOverride(contact, pathLen: null);
case _RoutingMode.flood:
setState(() => _syncStatus = null);
await connector.setPathOverride(contact, pathLen: -1);
case _RoutingMode.manual:
await _editManualPath(connector, contact);
}
}
Future<void> _editManualPath(
MeshCoreConnector connector,
Contact contact,
) async {
final override = contact.pathOverride;
final initial = override != null && override > 0
? (contact.pathOverrideBytes ?? Uint8List(0))
: (contact.pathLength > 0 ? contact.path : Uint8List(0));
final available = connector.allContacts
.where((c) => c.publicKeyHex != contact.publicKeyHex)
.toList();
final result = await PathEditorSheet.show(
context,
availableContacts: available,
initialPath: initial,
);
if (result == null || !mounted) return;
await connector.setPathOverride(
contact,
pathLen: result.length,
pathBytes: result,
);
await _verifyPath(connector, contact, result);
}
Future<void> _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<void> _verifyPath(
MeshCoreConnector connector,
Contact contact,
Uint8List bytes,
) async {
if (!mounted) return;
if (!connector.isConnected) {
setState(() => _syncStatus = context.l10n.chat_pathSavedLocally);
return;
}
setState(() => _syncStatus = null);
final verified = await connector.verifyContactPathOnDevice(contact, bytes);
if (!mounted) return;
setState(
() => _syncStatus = verified
? context.l10n.chat_pathDeviceConfirmed
: context.l10n.chat_pathDeviceNotConfirmed,
);
}
Future<void> _forgetPath(
MeshCoreConnector connector,
Contact contact,
) async {
await connector.clearContactPath(contact);
if (!mounted) return;
setState(() => _syncStatus = context.l10n.chat_pathCleared);
}
_PathQuality _qualityOf(PathRecord record, List<DirectRepeater> 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<int> pathBytes,
) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes),
flipPathAround: true,
targetContact: contact,
pathHashByteWidth: connector.pathHashByteWidth,
),
),
);
}
void _showPathDetail(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
List<int> pathBytes,
) {
final l10n = context.l10n;
final formattedPath = PathHelper.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 = <String>[
l10n.routing_deliveryCounts(stats.successCount, stats.failureCount),
if (stats.lastTripTimeMs > 0)
'${(stats.lastTripTimeMs / 1000).toStringAsFixed(1)}s',
if (stats.lastUsed != null)
l10n.routing_lastWorked(_relativeTime(context, stats.lastUsed!)),
];
return parts.join('');
}
Widget _floodTile(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
({
int successCount,
int failureCount,
int lastTripTimeMs,
DateTime? lastUsed,
}) stats,
) {
final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme;
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: CircleAvatar(
radius: 18,
backgroundColor: scheme.surfaceContainerHighest,
child: Icon(Icons.waves, size: 18, color: scheme.onSurfaceVariant),
),
title: Text(l10n.routing_floodDelivery),
subtitle: Text(
_floodStatsLine(context, stats),
style: const TextStyle(fontSize: 11),
),
trailing: mode == _RoutingMode.flood
? Icon(
Icons.check_circle,
color: scheme.primary,
semanticLabel: l10n.routing_inUse,
)
: null,
onTap: mode == _RoutingMode.flood
? null
: () => _selectMode(connector, contact, _RoutingMode.flood),
),
);
}
Widget _pathRecordTile(
BuildContext context,
MeshCoreConnector connector,
Contact contact,
_RoutingMode mode,
PathHistoryService pathService,
PathRecord record,
_PathQuality quality,
) {
final l10n = context.l10n;
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final (Color bg, Color fg) = switch (quality) {
_PathQuality.strong => (scheme.primaryContainer, scheme.onPrimaryContainer),
_PathQuality.good => (
scheme.secondaryContainer,
scheme.onSecondaryContainer,
),
_PathQuality.fair => (
scheme.tertiaryContainer,
scheme.onTertiaryContainer,
),
_PathQuality.proven => (
scheme.primaryContainer,
scheme.onPrimaryContainer,
),
_ => (scheme.surfaceContainerHighest, scheme.onSurfaceVariant),
};
final hasBytes = record.pathBytes.isNotEmpty;
final inUse = hasBytes &&
((mode == _RoutingMode.manual &&
listEquals(record.pathBytes, contact.pathOverrideBytes)) ||
(mode == _RoutingMode.auto &&
listEquals(record.pathBytes, contact.path)));
final title = hasBytes
? PathHelper.resolvePathNames(record.pathBytes, connector.allContacts)
: l10n.chat_hopsCount(record.hopCount);
final line1 =
'${l10n.chat_hopsCount(record.hopCount)}${_qualityLabel(context, quality)}';
final line2Parts = <String>[
record.timestamp != null
? l10n.routing_lastWorked(_relativeTime(context, record.timestamp!))
: l10n.routing_neverWorked,
if (record.tripTimeMs > 0)
'${(record.tripTimeMs / 1000).toStringAsFixed(1)}s',
l10n.routing_deliveryCounts(record.successCount, record.failureCount),
];
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
enabled: hasBytes,
leading: CircleAvatar(
radius: 18,
backgroundColor: bg,
child: Icon(
_qualityIcon(quality),
size: 18,
color: fg,
semanticLabel: _qualityLabel(context, quality),
),
),
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(
'$line1\n${line2Parts.join('')}',
style: const TextStyle(fontSize: 11),
),
isThreeLine: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (inUse)
Tooltip(
message: l10n.routing_inUse,
child: Icon(
Icons.check_circle,
color: scheme.primary,
semanticLabel: l10n.routing_inUse,
),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
tooltip: l10n.chat_removePath,
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
onPressed: () => pathService.removePathRecord(
contact.publicKeyHex,
record.pathBytes,
),
),
],
),
onTap: hasBytes && !inUse
? () => _applyHistoryPath(connector, contact, record)
: null,
onLongPress: hasBytes
? () =>
_showPathDetail(context, connector, contact, record.pathBytes)
: null,
),
);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final scheme = theme.colorScheme;
return Consumer2<MeshCoreConnector, PathHistoryService>(
builder: (context, connector, pathService, _) {
final contact = _resolveContact(connector);
final mode = _modeOf(contact);
final floodStats = pathService.getFloodStats(contact.publicKeyHex);
final hasFloodStats =
floodStats != null &&
(floodStats.successCount > 0 || floodStats.failureCount > 0);
final rankedRepeaters = List.of(connector.directRepeaters)
..sort((a, b) => b.ranking.compareTo(a.ranking));
final entries = pathService
.getRecentPaths(contact.publicKeyHex)
.map((r) => (quality: _qualityOf(r, rankedRepeaters), record: r))
.toList()
..sort((a, b) {
final byQuality = a.quality.index.compareTo(b.quality.index);
if (byQuality != 0) return byQuality;
final aTime =
a.record.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0);
final bTime =
b.record.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0);
return bTime.compareTo(aTime);
});
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
children: [
Center(
child: Container(
width: 32,
height: 4,
decoration: BoxDecoration(
color: scheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 12),
Text(l10n.routing_title, style: theme.textTheme.titleLarge),
Text(
contact.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
SegmentedButton<_RoutingMode>(
style: const ButtonStyle(
minimumSize: WidgetStatePropertyAll(Size.fromHeight(44)),
),
segments: [
ButtonSegment(
value: _RoutingMode.auto,
icon: const Icon(Icons.auto_mode),
label: Text(l10n.routing_modeAuto),
),
ButtonSegment(
value: _RoutingMode.flood,
icon: const Icon(Icons.waves),
label: Text(l10n.routing_modeFlood),
),
ButtonSegment(
value: _RoutingMode.manual,
icon: const Icon(Icons.edit_road),
label: Text(l10n.routing_modeManual),
),
],
selected: {mode},
onSelectionChanged: (selection) =>
_selectMode(connector, contact, selection.first),
),
const SizedBox(height: 8),
Text(
_modeHint(context, mode),
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
_currentRouteCard(context, connector, contact, mode, floodStats),
const SizedBox(height: 16),
Text(l10n.routing_knownPaths, style: theme.textTheme.titleSmall),
Text(
l10n.routing_knownPathsHint,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
if (hasFloodStats)
_floodTile(context, connector, contact, mode, floodStats),
if (entries.isEmpty && !hasFloodStats)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
l10n.chat_noPathHistoryYet,
style: theme.textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
),
...entries.map(
(entry) => _pathRecordTile(
context,
connector,
contact,
mode,
pathService,
entry.record,
entry.quality,
),
),
],
);
},
);
}
}
+1 -1
View File
@@ -166,7 +166,7 @@ class _SNRIndicatorState extends State<SNRIndicator> {
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.grey, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
+3 -3
View File
@@ -147,19 +147,19 @@ class _TelemetryLocationMapState extends State<TelemetryLocationMap> {
children: [ children: [
_MapButton( _MapButton(
icon: Icons.add, icon: Icons.add,
tooltip: 'Zoom in', tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomBy(1), onPressed: () => _zoomBy(1),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
_MapButton( _MapButton(
icon: Icons.remove, icon: Icons.remove,
tooltip: 'Zoom out', tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomBy(-1), onPressed: () => _zoomBy(-1),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
_MapButton( _MapButton(
icon: Icons.my_location, icon: Icons.my_location,
tooltip: 'Center map', tooltip: context.l10n.map_centerMap,
onPressed: () => onPressed: () =>
_mapController.move(_position, _initialZoom), _mapController.move(_position, _initialZoom),
), ),
+1 -1
View File
@@ -65,7 +65,7 @@ dependencies:
path_provider: ^2.1.5 path_provider: ^2.1.5
share_plus: ^12.0.1 share_plus: ^12.0.1
build_pipe: ^0.3.1 build_pipe: ^0.3.1
material_symbols_icons: ^4.2906.0 material_symbols_icons: ^4.2928.1
web: ^1.1.1 web: ^1.1.1
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
flutter_blue_plus_platform_interface: ^8.2.1 flutter_blue_plus_platform_interface: ^8.2.1
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Generate local PoCs for MeshCore clipboard contact import validation gaps.
The output is a meshcore:// URI suitable for manual testing in a local/dev app
session. It does not connect to BLE/USB/TCP devices or transmit anything.
"""
from __future__ import annotations
import argparse
import sys
SCHEME = "meshcore://"
def uri_from_bytes(payload: bytes) -> str:
return SCHEME + payload.hex()
def oversized_payload(size: int) -> bytes:
if size < 1:
raise ValueError("size must be positive")
return b"A" * size
def short_malformed_payload() -> bytes:
return b"\x00"
def non_advert_like_payload() -> bytes:
# 98 bytes matches the UI's minimum exported-contact length check, but the
# content is intentionally not a valid signed advert/contact packet.
payload = bytearray(98)
payload[0:4] = b"POC!"
payload[36] = 0xFF
payload[-4:] = b"END!"
return bytes(payload)
def main() -> int:
parser = argparse.ArgumentParser(
description="Generate meshcore:// clipboard import PoC payloads."
)
parser.add_argument(
"case",
choices=("short", "non-advert", "oversized"),
help="PoC case to generate.",
)
parser.add_argument(
"--size",
type=int,
default=4096,
help="Byte length for the oversized case. Default: 4096.",
)
args = parser.parse_args()
if args.case == "short":
payload = short_malformed_payload()
elif args.case == "non-advert":
payload = non_advert_like_payload()
else:
payload = oversized_payload(args.size)
sys.stdout.write(uri_from_bytes(payload))
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+4 -2
View File
@@ -123,11 +123,13 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.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(); await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsOneWidget); expect(find.byType(TcpScreen), findsOneWidget);
await tester.tap(find.widgetWithText(FloatingActionButton, 'Bluetooth')); await tester.tap(find.widgetWithText(OutlinedButton, 'Bluetooth'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(TcpScreen), findsNothing); expect(find.byType(TcpScreen), findsNothing);
+13 -5
View File
@@ -157,10 +157,18 @@ void main() {
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final context = tester.element(find.byType(ScannerScreen));
final l10n = AppLocalizations.of(context);
if (PlatformInfo.supportsUsbSerial) { if (PlatformInfo.supportsUsbSerial) {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget); expect(
find.byTooltip(l10n.connectionChoiceUsbLabel),
findsOneWidget,
);
} else { } else {
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing); expect(
find.byTooltip(l10n.connectionChoiceUsbLabel),
findsNothing,
);
} }
// ScannerScreen.dispose() schedules disconnect work that debounces notify. // ScannerScreen.dispose() schedules disconnect work that debounces notify.
@@ -186,13 +194,13 @@ void main() {
final context = tester.element(find.byType(ScannerScreen)); final context = tester.element(find.byType(ScannerScreen));
final l10n = AppLocalizations.of(context); final l10n = AppLocalizations.of(context);
expect(find.text(l10n.scanner_scan), findsOneWidget); expect(find.text(l10n.scanner_scan), findsWidgets);
if (PlatformInfo.supportsUsbSerial) { if (PlatformInfo.supportsUsbSerial) {
expect(find.text(l10n.connectionChoiceUsbLabel), findsOneWidget); expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsOneWidget);
} }
if (!PlatformInfo.isWeb) { if (!PlatformInfo.isWeb) {
expect(find.text(l10n.connectionChoiceTcpLabel), findsOneWidget); expect(find.byTooltip(l10n.connectionChoiceTcpLabel), findsOneWidget);
} }
await tester.pumpWidget(const SizedBox.shrink()); await tester.pumpWidget(const SizedBox.shrink());
+987 -1
View File
@@ -1 +1,987 @@
{} {
"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"
]
}