mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-05 15:32:58 +10:00
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:
+1
Submodule .claude/worktrees/agent-a01277dd8cf0dedb6 added at e37616fa15
+1
Submodule .claude/worktrees/agent-a0c7384491c5fc807 added at e37616fa15
+1
Submodule .claude/worktrees/agent-a1751d6addbf69e1d added at e37616fa15
+1
Submodule .claude/worktrees/agent-a2fad933ed81b10e2 added at e37616fa15
+1
Submodule .claude/worktrees/agent-a4d771c23db919c7f added at e37616fa15
+1
Submodule .claude/worktrees/agent-a6675128f3ee1d6a6 added at e37616fa15
+1
Submodule .claude/worktrees/agent-a894f10813e684f84 added at e37616fa15
+1
Submodule .claude/worktrees/agent-a8ba0537ad42768d7 added at e37616fa15
+1
Submodule .claude/worktrees/agent-a926d49d72757ce68 added at e37616fa15
+1
Submodule .claude/worktrees/agent-ad2e38b6608f23ab4 added at e37616fa15
+1
Submodule .claude/worktrees/agent-ada5836798aea71a1 added at e37616fa15
+1
Submodule .claude/worktrees/agent-aee10bd582e9e36cf added at e37616fa15
@@ -160,6 +160,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
{}; // contactPubKeyHex -> Set of "targetHash_emoji"
|
||||
|
||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||
StreamSubscription<bool>? _isScanningSubscription;
|
||||
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
|
||||
StreamSubscription<List<int>>? _notifySubscription;
|
||||
Timer? _notifyListenersTimer;
|
||||
@@ -507,10 +508,22 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (messages == null) return;
|
||||
final removed = messages.remove(message);
|
||||
if (!removed) return;
|
||||
_retryService?.untrack(message.messageId);
|
||||
await _messageStore.saveMessages(contactKeyHex, messages);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> resendMessage(Contact contact, Message message) async {
|
||||
await deleteMessage(message);
|
||||
await sendMessage(
|
||||
contact,
|
||||
message.text,
|
||||
originalText: message.originalText,
|
||||
translatedLanguageCode: message.translatedLanguageCode,
|
||||
translationModelId: message.translationModelId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadMessagesForContact(String contactKeyHex) async {
|
||||
if (_loadedConversationKeys.contains(contactKeyHex)) return;
|
||||
_loadedConversationKeys.add(contactKeyHex);
|
||||
@@ -1356,6 +1369,21 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}) async {
|
||||
if (_state == MeshCoreConnectionState.scanning) return;
|
||||
|
||||
// A BLE scan must never disturb an active (or in-progress) non-BLE
|
||||
// connection. The connection state enum is shared across transports, so
|
||||
// entering the `scanning` state while connected over TCP/USB would clobber
|
||||
// the live `connected` state and later reset it to `disconnected`.
|
||||
if (_state != MeshCoreConnectionState.disconnected ||
|
||||
_tcpConnector.isConnected ||
|
||||
_usbManager.isConnected) {
|
||||
_appDebugLogService?.warn(
|
||||
'startScan ignored: not idle (state=$_state, '
|
||||
'tcp=${_tcpConnector.isConnected}, usb=${_usbManager.isConnected})',
|
||||
tag: 'BLE Scan',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_scanResults.clear();
|
||||
_linuxSystemScanResults.clear();
|
||||
_setState(MeshCoreConnectionState.scanning);
|
||||
@@ -1409,20 +1437,40 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
});
|
||||
|
||||
try {
|
||||
// Filter by the Nordic UART Service UUID rather than by advertised
|
||||
// name. All MeshCore-compatible firmware (ESP32 + nRF52) advertises this
|
||||
// service UUID, so this matches every device regardless of the name it
|
||||
// chooses to advertise (e.g. community forks like the M5 Cardputer that
|
||||
// do not use a "MeshCore-" name prefix). This mirrors how the official
|
||||
// app discovers devices. Note: on Android `withKeywords` cannot be
|
||||
// combined with any other filter, which is why name keywords are not
|
||||
// used here.
|
||||
await FlutterBluePlus.startScan(
|
||||
withKeywords: MeshCoreUuids.deviceNamePrefixes,
|
||||
withServices: [Guid(MeshCoreUuids.service)],
|
||||
webOptionalServices: [Guid(MeshCoreUuids.service)],
|
||||
timeout: timeout,
|
||||
androidScanMode: AndroidScanMode.lowLatency,
|
||||
);
|
||||
} catch (error) {
|
||||
_appDebugLogService?.warn('Scan/picker failure: $error', tag: 'BLE Scan');
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
await stopScan();
|
||||
rethrow;
|
||||
}
|
||||
|
||||
await Future.delayed(timeout);
|
||||
await stopScan();
|
||||
// Reset our shared state when the native scan ends — whether it was stopped
|
||||
// by the user (stopScan), by the platform timeout, or by Bluetooth turning
|
||||
// off. This replaces a blocking `Future.delayed(timeout)` tail that kept
|
||||
// startScan() pending for the whole timeout and made Stop appear ineffective.
|
||||
// `isScanning` is a re-emit stream that replays its latest value on listen,
|
||||
// so skip(1) to ignore that and only react to a genuine transition to false.
|
||||
await _isScanningSubscription?.cancel();
|
||||
_isScanningSubscription = FlutterBluePlus.isScanning.skip(1).listen((
|
||||
scanning,
|
||||
) {
|
||||
if (!scanning && _state == MeshCoreConnectionState.scanning) {
|
||||
unawaited(stopScan());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadLinuxSystemDevicesForScan() async {
|
||||
@@ -1430,15 +1478,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final systemDevices = await FlutterBluePlus.systemDevices([
|
||||
Guid(MeshCoreUuids.service),
|
||||
]);
|
||||
// systemDevices is already filtered by the NUS service UUID above, so no
|
||||
// additional name-prefix filtering is applied here. This keeps Linux
|
||||
// discovery name-agnostic and consistent with the main scan path.
|
||||
_linuxSystemScanResults
|
||||
..clear()
|
||||
..addAll(
|
||||
systemDevices
|
||||
.where(
|
||||
(device) => MeshCoreUuids.deviceNamePrefixes.any(
|
||||
device.platformName.startsWith,
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(device) => ScanResult(
|
||||
device: device,
|
||||
@@ -1499,9 +1545,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
await _scanSubscription?.cancel();
|
||||
_scanSubscription = null;
|
||||
await _isScanningSubscription?.cancel();
|
||||
_isScanningSubscription = null;
|
||||
|
||||
if (_state == MeshCoreConnectionState.scanning) {
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
// Restore to `connected` if a non-BLE transport is still live, so a stray
|
||||
// scan can never tear down the reported connection state. Normally there
|
||||
// is no live transport here and we fall through to `disconnected`.
|
||||
final restored = (_tcpConnector.isConnected || _usbManager.isConnected)
|
||||
? MeshCoreConnectionState.connected
|
||||
: MeshCoreConnectionState.disconnected;
|
||||
_setState(restored);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1743,6 +1797,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
activeTransport == MeshCoreTransportType.tcp;
|
||||
}
|
||||
|
||||
/// Fast (non-timeout) connect failures are usually a stale link left over
|
||||
/// from a previous session and recover on an immediate retry. Timeouts mean
|
||||
/// the device is likely off or out of range, so retrying would only delay
|
||||
/// genuine failure feedback.
|
||||
@visibleForTesting
|
||||
static bool shouldRetryBleConnectAfterError(String errorText) {
|
||||
final lowerErrorText = errorText.toLowerCase();
|
||||
return !lowerErrorText.contains('timed out') &&
|
||||
!lowerErrorText.contains('timeout');
|
||||
}
|
||||
|
||||
Future<void> connect(
|
||||
BluetoothDevice device, {
|
||||
String? displayName,
|
||||
@@ -1911,18 +1976,71 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await device.connect(
|
||||
Future<void> attemptConnect() {
|
||||
return device.connect(
|
||||
timeout: connectTimeout,
|
||||
mtu: null,
|
||||
license: License.free,
|
||||
);
|
||||
}
|
||||
|
||||
// A previous app session (e.g. killed from the iOS app switcher) can
|
||||
// leave the OS holding a stale link to the peripheral. Clear it before
|
||||
// connecting so the fresh attempt doesn't race the stale handle.
|
||||
if (!PlatformInfo.isWeb && device.isConnected) {
|
||||
_appDebugLogService?.warn(
|
||||
'Device reports an existing connection before connect; clearing stale link',
|
||||
tag: 'BLE Connect',
|
||||
);
|
||||
try {
|
||||
await device.disconnect(queue: false);
|
||||
} catch (cleanupError) {
|
||||
_appDebugLogService?.warn(
|
||||
'Stale-link cleanup disconnect failed (continuing): $cleanupError',
|
||||
tag: 'BLE Connect',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await attemptConnect();
|
||||
} catch (error) {
|
||||
_appDebugLogService?.error(
|
||||
'device.connect() failure: $error',
|
||||
tag: 'BLE Connect',
|
||||
);
|
||||
rethrow;
|
||||
if (PlatformInfo.isWeb ||
|
||||
!shouldRetryBleConnectAfterError(error.toString())) {
|
||||
rethrow;
|
||||
}
|
||||
// Fast (non-timeout) failures are usually a stale connection left by
|
||||
// a previous session; clean up and retry once before surfacing.
|
||||
_appDebugLogService?.warn(
|
||||
'Retrying connect once after clearing possible stale connection',
|
||||
tag: 'BLE Connect',
|
||||
);
|
||||
try {
|
||||
await device.disconnect(queue: false);
|
||||
} catch (cleanupError) {
|
||||
_appDebugLogService?.warn(
|
||||
'Pre-retry cleanup disconnect failed (continuing): $cleanupError',
|
||||
tag: 'BLE Connect',
|
||||
);
|
||||
}
|
||||
await Future<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3237,6 +3355,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(buildRemoveContactFrame(contact.publicKey));
|
||||
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
||||
_knownContactKeys.remove(contact.publicKeyHex);
|
||||
unawaited(updateKnownDiscovered());
|
||||
unawaited(_persistContacts());
|
||||
_conversations.remove(contact.publicKeyHex);
|
||||
_loadedConversationKeys.remove(contact.publicKeyHex);
|
||||
@@ -3450,12 +3569,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
Future<void> sendCliCommand(String command) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
// CLI commands are sent as UTF-8 text with a special prefix
|
||||
final commandBytes = utf8.encode(command);
|
||||
final bytes = Uint8List.fromList([0x01, ...commandBytes, 0x00]);
|
||||
final selfKey = _selfPublicKey;
|
||||
if (selfKey == null) return;
|
||||
_lastSentWasCliCommand = true;
|
||||
await sendFrame(bytes);
|
||||
await sendFrame(buildSendCliCommandFrame(selfKey, command));
|
||||
}
|
||||
|
||||
Future<void> setNodeName(String name) async {
|
||||
@@ -3929,8 +4046,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_advertLocPolicy = reader.readByte();
|
||||
final telemetryFlag = reader.readByte();
|
||||
_telemetryModeBase = telemetryFlag & 0x03;
|
||||
_telemetryModeEnv = telemetryFlag >> 2 & 0x03;
|
||||
_telemetryModeLoc = telemetryFlag >> 4 & 0x03;
|
||||
_telemetryModeLoc = telemetryFlag >> 2 & 0x03;
|
||||
_telemetryModeEnv = telemetryFlag >> 4 & 0x03;
|
||||
|
||||
_manualAddContacts = reader.readByte() & 0x01 == 0x00;
|
||||
|
||||
@@ -4275,7 +4392,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
tag: 'Connector',
|
||||
);
|
||||
notifyListeners();
|
||||
removeContact(contactTmp);
|
||||
unawaited(
|
||||
removeContact(contactTmp).catchError(
|
||||
(e) => appLogger.warn(
|
||||
'Failed to remove self contact: $e',
|
||||
tag: 'Connector',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final contact = getFromDiscovered(contactTmp);
|
||||
@@ -5521,6 +5645,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
messages.add(message);
|
||||
if (messages.length > _messageWindowSize) {
|
||||
messages.removeRange(0, messages.length - _messageWindowSize);
|
||||
}
|
||||
_messageStore.saveMessages(pubKeyHex, messages);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -5907,20 +6034,24 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) {
|
||||
if (existing.text != incoming.text) return false;
|
||||
|
||||
// Self-echo: an outgoing message coming back via a repeater. The send is
|
||||
// delayed by _waitForRadioQuiet (often 10s+) and propagation can add more,
|
||||
// so the timestamp gap can easily exceed the cross-peer window.
|
||||
final selfName = _selfName ?? 'Me';
|
||||
final isSelfEcho =
|
||||
existing.isOutgoing &&
|
||||
!incoming.isOutgoing &&
|
||||
(incoming.senderName == selfName || existing.senderName == selfName);
|
||||
|
||||
final windowMs = isSelfEcho ? 10 * 60 * 1000 : 30000;
|
||||
final diffMs =
|
||||
(existing.timestamp.millisecondsSinceEpoch -
|
||||
incoming.timestamp.millisecondsSinceEpoch)
|
||||
.abs();
|
||||
if (diffMs > 30000) return false;
|
||||
if (diffMs > windowMs) return false;
|
||||
|
||||
if (existing.senderName == incoming.senderName) return true;
|
||||
|
||||
if (existing.isOutgoing && !incoming.isOutgoing) {
|
||||
final selfName = _selfName ?? 'Me';
|
||||
if (incoming.senderName == selfName || existing.senderName == selfName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isSelfEcho) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -6152,6 +6283,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
@override
|
||||
void dispose() {
|
||||
_scanSubscription?.cancel();
|
||||
_isScanningSubscription?.cancel();
|
||||
_connectionSubscription?.cancel();
|
||||
_usbFrameSubscription?.cancel();
|
||||
_notifySubscription?.cancel();
|
||||
@@ -6210,82 +6342,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void importContact(Uint8List frame) {
|
||||
final packet = BufferReader(frame);
|
||||
int payloadType = 0;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
try {
|
||||
packet.skipBytes(1); // Skip frame type byte
|
||||
packet.skipBytes(1); // Skip SNR byte
|
||||
packet.skipBytes(1); // Skip RSSI byte
|
||||
final header = packet.readByte();
|
||||
final routeType = header & 0x03;
|
||||
payloadType = (header >> 2) & 0x0F;
|
||||
if (routeType == _routeTransportFlood ||
|
||||
routeType == _routeTransportDirect) {
|
||||
packet.skipBytes(4); // Skip transport-specific bytes
|
||||
}
|
||||
//final payloadVer = (header >> 6) & 0x03;
|
||||
final pathLenRaw = packet.readByte();
|
||||
final pathByteLen = _decodePathByteLen(pathLenRaw);
|
||||
pathBytes = packet.readBytes(pathByteLen);
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
double? latitude;
|
||||
double? longitude;
|
||||
String name = '';
|
||||
Uint8List publicKey = Uint8List(0);
|
||||
int type = 0;
|
||||
int timestamp = 0;
|
||||
bool hasLocation = false;
|
||||
bool hasName = false;
|
||||
if (payloadType != payloadTypeADVERT) {
|
||||
appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
publicKey = packet.readBytes(32);
|
||||
timestamp = packet.readInt32LE();
|
||||
//TODO add signature verification
|
||||
packet.skipBytes(64); // Skip signature for now
|
||||
final flags = packet.readByte();
|
||||
type = flags & 0x0F;
|
||||
hasLocation = (flags & 0x10) != 0;
|
||||
// For future use:
|
||||
//final hasFeature1 = (flags & 0x20) != 0;
|
||||
//final hasFeature2 = (flags & 0x40) != 0;
|
||||
hasName = (flags & 0x80) != 0;
|
||||
if (hasLocation && packet.remaining >= 8) {
|
||||
latitude = packet.readInt32LE() / 1e6;
|
||||
longitude = packet.readInt32LE() / 1e6;
|
||||
}
|
||||
if (hasName && packet.remaining > 0) {
|
||||
name = packet.readCString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
|
||||
importDiscoveredContact(
|
||||
Contact(
|
||||
rawPacket: frame,
|
||||
publicKey: publicKey,
|
||||
name: name,
|
||||
type: type,
|
||||
pathLength: pathBytes.isEmpty ? -1 : pathBytes.length,
|
||||
path: Uint8List.fromList(
|
||||
pathBytes.reversed.toList(),
|
||||
), // Store path in reverse for easier use in outgoing messages
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool hasValidLocation(double? latitude, double? longitude) {
|
||||
const double epsilon = 1e-6;
|
||||
final lat = latitude ?? 0.0;
|
||||
|
||||
@@ -457,8 +457,13 @@ String pubKeyToHex(Uint8List pubKey) {
|
||||
|
||||
// Helper to convert hex string to public key
|
||||
Uint8List hexToPubKey(String hex) {
|
||||
if (hex.length != pubKeySize * 2) {
|
||||
throw FormatException(
|
||||
'Public key hex must be ${pubKeySize * 2} chars, got ${hex.length}',
|
||||
);
|
||||
}
|
||||
final result = Uint8List(pubKeySize);
|
||||
for (int i = 0; i < pubKeySize && i * 2 + 1 < hex.length; i++) {
|
||||
for (int i = 0; i < pubKeySize; i++) {
|
||||
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -3,6 +3,10 @@ class MeshCoreUuids {
|
||||
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
|
||||
/// Known advertised-name prefixes used by stock MeshCore firmware builds.
|
||||
/// Discovery no longer filters on these (it filters on the [service] UUID so
|
||||
/// that community forks with custom names are still found); kept for
|
||||
/// reference and possible future display heuristics.
|
||||
static const List<String> deviceNamePrefixes = [
|
||||
"MeshCore-",
|
||||
"Whisper-",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -8,24 +8,29 @@ class PathHelper {
|
||||
.join(',');
|
||||
}
|
||||
|
||||
static String hopHex(int byte) {
|
||||
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
}
|
||||
|
||||
static String? hopName(int byte, List<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(
|
||||
List<int> pathBytes,
|
||||
List<Contact> allContacts,
|
||||
) {
|
||||
return pathBytes
|
||||
.map((b) {
|
||||
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final matches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKey.first == b &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||
)
|
||||
.toList();
|
||||
if (matches.isEmpty) return hex;
|
||||
if (matches.length == 1) return matches.first.name;
|
||||
return matches.map((c) => c.name).join(' | ');
|
||||
})
|
||||
.map((b) => hopName(b, allContacts) ?? hopHex(b))
|
||||
.join(' \u2192 ');
|
||||
}
|
||||
}
|
||||
|
||||
+89
-73
@@ -28,6 +28,12 @@
|
||||
"common_remove": "Remove",
|
||||
"common_enable": "Enable",
|
||||
"common_disable": "Disable",
|
||||
"common_undo": "Undo",
|
||||
"messageStatus_sent": "Sent",
|
||||
"messageStatus_delivered": "Delivered",
|
||||
"messageStatus_pending": "Sending",
|
||||
"messageStatus_failed": "Failed to send",
|
||||
"messageStatus_repeated": "Heard repeated",
|
||||
"common_reboot": "Reboot",
|
||||
"common_loading": "Loading...",
|
||||
"common_notAvailable": "—",
|
||||
@@ -297,17 +303,6 @@
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
|
||||
"appSettings_maxMessageRetries": "Max Message Retries",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_battery": "Battery",
|
||||
"appSettings_batteryChemistry": "Battery Chemistry",
|
||||
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
|
||||
@@ -453,6 +448,9 @@
|
||||
}
|
||||
},
|
||||
"contacts_newGroup": "New Group",
|
||||
"contacts_moreOptions": "More options",
|
||||
"contacts_searchOpen": "Search contacts",
|
||||
"contacts_searchClose": "Close search",
|
||||
"contacts_groupName": "Group name",
|
||||
"contacts_groupNameRequired": "Group name is required",
|
||||
"contacts_groupNameReserved": "This group name is reserved",
|
||||
@@ -777,15 +775,6 @@
|
||||
}
|
||||
},
|
||||
"debugFrame_hexDump": "Hex Dump:",
|
||||
"chat_pathManagement": "Path Management",
|
||||
"chat_ShowAllPaths": "Show all paths",
|
||||
"chat_routingMode": "Routing mode",
|
||||
"chat_autoUseSavedPath": "Auto (use saved path)",
|
||||
"chat_forceFloodMode": "Force Flood Mode",
|
||||
"chat_recentAckPaths": "Recent ACK Paths (tap to use):",
|
||||
"chat_pathHistoryFull": "Path history is full. Remove entries to add new ones.",
|
||||
"chat_hopSingular": "hop",
|
||||
"chat_hopPlural": "hops",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
@@ -794,31 +783,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_successes": "successes",
|
||||
"chat_score": "Score",
|
||||
"chat_removePath": "Remove path",
|
||||
"chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.",
|
||||
"chat_pathActions": "Path Actions:",
|
||||
"chat_setCustomPath": "Set Custom Path",
|
||||
"chat_setCustomPathSubtitle": "Manually specify routing path",
|
||||
"chat_clearPath": "Clear Path",
|
||||
"chat_clearPathSubtitle": "Force rediscovery on next send",
|
||||
"chat_pathCleared": "Path cleared. Next message will rediscover route.",
|
||||
"chat_floodModeSubtitle": "Use routing toggle in app bar",
|
||||
"chat_floodModeEnabled": "Flood mode enabled. Toggle back via routing icon in app bar.",
|
||||
"chat_fullPath": "Full Path",
|
||||
"chat_pathDetailsNotAvailable": "Path details not available yet. Try sending a message to refresh.",
|
||||
"chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
|
||||
"@chat_pathSetHops": {
|
||||
"routing_title": "Routing",
|
||||
"routing_modeAuto": "Auto",
|
||||
"routing_modeFlood": "Flood",
|
||||
"routing_modeManual": "Manual",
|
||||
"routing_modeAutoHint": "Picks the best known path automatically, flooding when none is known.",
|
||||
"routing_modeFloodHint": "Broadcasts through every repeater. Most reliable, but uses more airtime.",
|
||||
"routing_modeManualHint": "Always sends along the exact path you set.",
|
||||
"routing_currentRoute": "Current route",
|
||||
"routing_directNoHops": "Direct — no repeater hops",
|
||||
"routing_noPathYet": "No path yet. The next message floods until a route is discovered.",
|
||||
"routing_floodBroadcast": "Broadcast through every repeater",
|
||||
"routing_editPath": "Edit path",
|
||||
"routing_forgetPath": "Forget path",
|
||||
"routing_knownPaths": "Known paths",
|
||||
"routing_knownPathsHint": "Tap a path to switch to it.",
|
||||
"routing_inUse": "In use",
|
||||
"routing_qualityStrong": "Strong first hop",
|
||||
"routing_qualityGood": "Good first hop",
|
||||
"routing_qualityFair": "Fair first hop",
|
||||
"routing_qualityWorked": "Has delivered",
|
||||
"routing_qualityFlood": "Heard via flood",
|
||||
"routing_qualityUntested": "Untested",
|
||||
"routing_lastWorked": "worked {when}",
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"hopCount": {
|
||||
"type": "int"
|
||||
},
|
||||
"status": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"routing_neverWorked": "never confirmed",
|
||||
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
"type": "int"
|
||||
},
|
||||
"failures": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"routing_floodDelivery": "Flood delivery",
|
||||
"pathEditor_title": "Build Path",
|
||||
"pathEditor_hopCounter": "{count} of 64 hops",
|
||||
"@pathEditor_hopCounter": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathEditor_noHops": "No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.",
|
||||
"pathEditor_addHops": "Add hops in order",
|
||||
"pathEditor_searchRepeaters": "Search repeaters",
|
||||
"pathEditor_advancedHex": "Advanced: raw hex path",
|
||||
"pathEditor_hexLabel": "Hex prefixes",
|
||||
"pathEditor_hexHelper": "Two hex characters per hop, separated by commas",
|
||||
"pathEditor_invalidTokens": "Invalid: {tokens}",
|
||||
"@pathEditor_invalidTokens": {
|
||||
"placeholders": {
|
||||
"tokens": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathEditor_tooManyHops": "Maximum 64 hops",
|
||||
"pathEditor_usePath": "Use this path",
|
||||
"pathEditor_removeHop": "Remove hop",
|
||||
"pathEditor_unknownHop": "Unknown repeater",
|
||||
"chat_pathSavedLocally": "Saved locally. Connect to sync.",
|
||||
"chat_pathDeviceConfirmed": "Device confirmed.",
|
||||
"chat_pathDeviceNotConfirmed": "Device not confirmed yet.",
|
||||
@@ -1100,41 +1138,8 @@
|
||||
"login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.",
|
||||
"common_reload": "Reload",
|
||||
"common_clear": "Clear",
|
||||
"path_currentPath": "Current path: {path}",
|
||||
"@path_currentPath": {
|
||||
"placeholders": {
|
||||
"path": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path",
|
||||
"@path_usingHopsPath": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_enterCustomPath": "Enter Custom Path",
|
||||
"path_currentPathLabel": "Current path",
|
||||
"path_hexPrefixInstructions": "Enter 2-character hex prefixes for each hop, separated by commas.",
|
||||
"path_hexPrefixExample": "Example: A1,F2,3C (each node uses first byte of its public key)",
|
||||
"path_labelHexPrefixes": "Path (hex prefixes)",
|
||||
"path_helperMaxHops": "Max 64 hops. Each prefix is 2 hex characters (1 byte)",
|
||||
"path_selectFromContacts": "Or select from contacts:",
|
||||
"path_noRepeatersFound": "No repeaters or room servers found.",
|
||||
"path_customPathsRequire": "Custom paths require intermediate hops that can relay messages.",
|
||||
"path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}",
|
||||
"@path_invalidHexPrefixes": {
|
||||
"placeholders": {
|
||||
"prefixes": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_tooLong": "Path too long. Maximum 64 hops allowed.",
|
||||
"path_setPath": "Set Path",
|
||||
"repeater_management": "Repeater Management",
|
||||
"room_management": "Room Server Management",
|
||||
"repeater_guest": "Repeater Information",
|
||||
@@ -1161,9 +1166,6 @@
|
||||
},
|
||||
"repeater_statusTitle": "Repeater Status",
|
||||
"repeater_routingMode": "Routing mode",
|
||||
"repeater_autoUseSavedPath": "Auto (use saved path)",
|
||||
"repeater_forceFloodMode": "Force Flood Mode",
|
||||
"repeater_pathManagement": "Path management",
|
||||
"repeater_refresh": "Refresh",
|
||||
"repeater_statusRequestTimeout": "Status request timed out.",
|
||||
"repeater_errorLoadingStatus": "Error loading status: {error}",
|
||||
@@ -2485,5 +2487,19 @@
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown"
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"map_zoomIn": "Zoom in",
|
||||
"map_zoomOut": "Zoom out",
|
||||
"map_centerMap": "Center map",
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth requires a Chromium browser",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"@channels_communityShortId": {
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_legendGpsConfirmed": "GPS confirmed",
|
||||
"pathTrace_legendInferred": "Inferred position"
|
||||
}
|
||||
|
||||
+324
-210
@@ -298,6 +298,42 @@ abstract class AppLocalizations {
|
||||
/// **'Disable'**
|
||||
String get common_disable;
|
||||
|
||||
/// No description provided for @common_undo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Undo'**
|
||||
String get common_undo;
|
||||
|
||||
/// No description provided for @messageStatus_sent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sent'**
|
||||
String get messageStatus_sent;
|
||||
|
||||
/// No description provided for @messageStatus_delivered.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delivered'**
|
||||
String get messageStatus_delivered;
|
||||
|
||||
/// No description provided for @messageStatus_pending.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sending'**
|
||||
String get messageStatus_pending;
|
||||
|
||||
/// No description provided for @messageStatus_failed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to send'**
|
||||
String get messageStatus_failed;
|
||||
|
||||
/// No description provided for @messageStatus_repeated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Heard repeated'**
|
||||
String get messageStatus_repeated;
|
||||
|
||||
/// No description provided for @common_reboot.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1534,12 +1570,6 @@ abstract class AppLocalizations {
|
||||
/// **'Number of retry attempts before marking a message as failed'**
|
||||
String get appSettings_maxMessageRetriesSubtitle;
|
||||
|
||||
/// No description provided for @path_routeWeight.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{weight}/{max}'**
|
||||
String path_routeWeight(String weight, String max);
|
||||
|
||||
/// No description provided for @appSettings_battery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1882,6 +1912,24 @@ abstract class AppLocalizations {
|
||||
/// **'New Group'**
|
||||
String get contacts_newGroup;
|
||||
|
||||
/// No description provided for @contacts_moreOptions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'More options'**
|
||||
String get contacts_moreOptions;
|
||||
|
||||
/// No description provided for @contacts_searchOpen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search contacts'**
|
||||
String get contacts_searchOpen;
|
||||
|
||||
/// No description provided for @contacts_searchClose.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Close search'**
|
||||
String get contacts_searchClose;
|
||||
|
||||
/// No description provided for @contacts_groupName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2710,78 +2758,12 @@ abstract class AppLocalizations {
|
||||
/// **'Hex Dump:'**
|
||||
String get debugFrame_hexDump;
|
||||
|
||||
/// No description provided for @chat_pathManagement.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path Management'**
|
||||
String get chat_pathManagement;
|
||||
|
||||
/// No description provided for @chat_ShowAllPaths.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show all paths'**
|
||||
String get chat_ShowAllPaths;
|
||||
|
||||
/// No description provided for @chat_routingMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Routing mode'**
|
||||
String get chat_routingMode;
|
||||
|
||||
/// No description provided for @chat_autoUseSavedPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto (use saved path)'**
|
||||
String get chat_autoUseSavedPath;
|
||||
|
||||
/// No description provided for @chat_forceFloodMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Force Flood Mode'**
|
||||
String get chat_forceFloodMode;
|
||||
|
||||
/// No description provided for @chat_recentAckPaths.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recent ACK Paths (tap to use):'**
|
||||
String get chat_recentAckPaths;
|
||||
|
||||
/// No description provided for @chat_pathHistoryFull.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path history is full. Remove entries to add new ones.'**
|
||||
String get chat_pathHistoryFull;
|
||||
|
||||
/// No description provided for @chat_hopSingular.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'hop'**
|
||||
String get chat_hopSingular;
|
||||
|
||||
/// No description provided for @chat_hopPlural.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'hops'**
|
||||
String get chat_hopPlural;
|
||||
|
||||
/// No description provided for @chat_hopsCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} {count, plural, =1{hop} other{hops}}'**
|
||||
String chat_hopsCount(int count);
|
||||
|
||||
/// No description provided for @chat_successes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'successes'**
|
||||
String get chat_successes;
|
||||
|
||||
/// No description provided for @chat_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Score'**
|
||||
String get chat_score;
|
||||
|
||||
/// No description provided for @chat_removePath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2794,71 +2776,251 @@ abstract class AppLocalizations {
|
||||
/// **'No path history yet.\nSend a message to discover paths.'**
|
||||
String get chat_noPathHistoryYet;
|
||||
|
||||
/// No description provided for @chat_pathActions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path Actions:'**
|
||||
String get chat_pathActions;
|
||||
|
||||
/// No description provided for @chat_setCustomPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set Custom Path'**
|
||||
String get chat_setCustomPath;
|
||||
|
||||
/// No description provided for @chat_setCustomPathSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manually specify routing path'**
|
||||
String get chat_setCustomPathSubtitle;
|
||||
|
||||
/// No description provided for @chat_clearPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear Path'**
|
||||
String get chat_clearPath;
|
||||
|
||||
/// No description provided for @chat_clearPathSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Force rediscovery on next send'**
|
||||
String get chat_clearPathSubtitle;
|
||||
|
||||
/// No description provided for @chat_pathCleared.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path cleared. Next message will rediscover route.'**
|
||||
String get chat_pathCleared;
|
||||
|
||||
/// No description provided for @chat_floodModeSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use routing toggle in app bar'**
|
||||
String get chat_floodModeSubtitle;
|
||||
|
||||
/// No description provided for @chat_floodModeEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Flood mode enabled. Toggle back via routing icon in app bar.'**
|
||||
String get chat_floodModeEnabled;
|
||||
|
||||
/// No description provided for @chat_fullPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Full Path'**
|
||||
String get chat_fullPath;
|
||||
|
||||
/// No description provided for @chat_pathDetailsNotAvailable.
|
||||
/// No description provided for @routing_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path details not available yet. Try sending a message to refresh.'**
|
||||
String get chat_pathDetailsNotAvailable;
|
||||
/// **'Routing'**
|
||||
String get routing_title;
|
||||
|
||||
/// No description provided for @chat_pathSetHops.
|
||||
/// No description provided for @routing_modeAuto.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}'**
|
||||
String chat_pathSetHops(int hopCount, String status);
|
||||
/// **'Auto'**
|
||||
String get routing_modeAuto;
|
||||
|
||||
/// No description provided for @routing_modeFlood.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Flood'**
|
||||
String get routing_modeFlood;
|
||||
|
||||
/// No description provided for @routing_modeManual.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manual'**
|
||||
String get routing_modeManual;
|
||||
|
||||
/// No description provided for @routing_modeAutoHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Picks the best known path automatically, flooding when none is known.'**
|
||||
String get routing_modeAutoHint;
|
||||
|
||||
/// No description provided for @routing_modeFloodHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Broadcasts through every repeater. Most reliable, but uses more airtime.'**
|
||||
String get routing_modeFloodHint;
|
||||
|
||||
/// No description provided for @routing_modeManualHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Always sends along the exact path you set.'**
|
||||
String get routing_modeManualHint;
|
||||
|
||||
/// No description provided for @routing_currentRoute.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Current route'**
|
||||
String get routing_currentRoute;
|
||||
|
||||
/// No description provided for @routing_directNoHops.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Direct — no repeater hops'**
|
||||
String get routing_directNoHops;
|
||||
|
||||
/// No description provided for @routing_noPathYet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No path yet. The next message floods until a route is discovered.'**
|
||||
String get routing_noPathYet;
|
||||
|
||||
/// No description provided for @routing_floodBroadcast.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Broadcast through every repeater'**
|
||||
String get routing_floodBroadcast;
|
||||
|
||||
/// No description provided for @routing_editPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit path'**
|
||||
String get routing_editPath;
|
||||
|
||||
/// No description provided for @routing_forgetPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Forget path'**
|
||||
String get routing_forgetPath;
|
||||
|
||||
/// No description provided for @routing_knownPaths.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Known paths'**
|
||||
String get routing_knownPaths;
|
||||
|
||||
/// No description provided for @routing_knownPathsHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap a path to switch to it.'**
|
||||
String get routing_knownPathsHint;
|
||||
|
||||
/// No description provided for @routing_inUse.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'In use'**
|
||||
String get routing_inUse;
|
||||
|
||||
/// No description provided for @routing_qualityStrong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Strong first hop'**
|
||||
String get routing_qualityStrong;
|
||||
|
||||
/// No description provided for @routing_qualityGood.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Good first hop'**
|
||||
String get routing_qualityGood;
|
||||
|
||||
/// No description provided for @routing_qualityFair.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fair first hop'**
|
||||
String get routing_qualityFair;
|
||||
|
||||
/// No description provided for @routing_qualityWorked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Has delivered'**
|
||||
String get routing_qualityWorked;
|
||||
|
||||
/// No description provided for @routing_qualityFlood.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Heard via flood'**
|
||||
String get routing_qualityFlood;
|
||||
|
||||
/// No description provided for @routing_qualityUntested.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Untested'**
|
||||
String get routing_qualityUntested;
|
||||
|
||||
/// No description provided for @routing_lastWorked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'worked {when}'**
|
||||
String routing_lastWorked(String when);
|
||||
|
||||
/// No description provided for @routing_neverWorked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'never confirmed'**
|
||||
String get routing_neverWorked;
|
||||
|
||||
/// No description provided for @routing_deliveryCounts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{successes} delivered, {failures} failed'**
|
||||
String routing_deliveryCounts(int successes, int failures);
|
||||
|
||||
/// No description provided for @routing_floodDelivery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Flood delivery'**
|
||||
String get routing_floodDelivery;
|
||||
|
||||
/// No description provided for @pathEditor_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Build Path'**
|
||||
String get pathEditor_title;
|
||||
|
||||
/// No description provided for @pathEditor_hopCounter.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} of 64 hops'**
|
||||
String pathEditor_hopCounter(int count);
|
||||
|
||||
/// No description provided for @pathEditor_noHops.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.'**
|
||||
String get pathEditor_noHops;
|
||||
|
||||
/// No description provided for @pathEditor_addHops.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add hops in order'**
|
||||
String get pathEditor_addHops;
|
||||
|
||||
/// No description provided for @pathEditor_searchRepeaters.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search repeaters'**
|
||||
String get pathEditor_searchRepeaters;
|
||||
|
||||
/// No description provided for @pathEditor_advancedHex.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Advanced: raw hex path'**
|
||||
String get pathEditor_advancedHex;
|
||||
|
||||
/// No description provided for @pathEditor_hexLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hex prefixes'**
|
||||
String get pathEditor_hexLabel;
|
||||
|
||||
/// No description provided for @pathEditor_hexHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Two hex characters per hop, separated by commas'**
|
||||
String get pathEditor_hexHelper;
|
||||
|
||||
/// No description provided for @pathEditor_invalidTokens.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid: {tokens}'**
|
||||
String pathEditor_invalidTokens(String tokens);
|
||||
|
||||
/// No description provided for @pathEditor_tooManyHops.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Maximum 64 hops'**
|
||||
String get pathEditor_tooManyHops;
|
||||
|
||||
/// No description provided for @pathEditor_usePath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use this path'**
|
||||
String get pathEditor_usePath;
|
||||
|
||||
/// No description provided for @pathEditor_removeHop.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove hop'**
|
||||
String get pathEditor_removeHop;
|
||||
|
||||
/// No description provided for @pathEditor_unknownHop.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unknown repeater'**
|
||||
String get pathEditor_unknownHop;
|
||||
|
||||
/// No description provided for @chat_pathSavedLocally.
|
||||
///
|
||||
@@ -3687,90 +3849,18 @@ abstract class AppLocalizations {
|
||||
/// **'Clear'**
|
||||
String get common_clear;
|
||||
|
||||
/// No description provided for @path_currentPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Current path: {path}'**
|
||||
String path_currentPath(String path);
|
||||
|
||||
/// No description provided for @path_usingHopsPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Using {count} {count, plural, =1{hop} other{hops}} path'**
|
||||
String path_usingHopsPath(int count);
|
||||
|
||||
/// No description provided for @path_enterCustomPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter Custom Path'**
|
||||
String get path_enterCustomPath;
|
||||
|
||||
/// No description provided for @path_currentPathLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Current path'**
|
||||
String get path_currentPathLabel;
|
||||
|
||||
/// No description provided for @path_hexPrefixInstructions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter 2-character hex prefixes for each hop, separated by commas.'**
|
||||
String get path_hexPrefixInstructions;
|
||||
|
||||
/// No description provided for @path_hexPrefixExample.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Example: A1,F2,3C (each node uses first byte of its public key)'**
|
||||
String get path_hexPrefixExample;
|
||||
|
||||
/// No description provided for @path_labelHexPrefixes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path (hex prefixes)'**
|
||||
String get path_labelHexPrefixes;
|
||||
|
||||
/// No description provided for @path_helperMaxHops.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Max 64 hops. Each prefix is 2 hex characters (1 byte)'**
|
||||
String get path_helperMaxHops;
|
||||
|
||||
/// No description provided for @path_selectFromContacts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Or select from contacts:'**
|
||||
String get path_selectFromContacts;
|
||||
|
||||
/// No description provided for @path_noRepeatersFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No repeaters or room servers found.'**
|
||||
String get path_noRepeatersFound;
|
||||
|
||||
/// No description provided for @path_customPathsRequire.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Custom paths require intermediate hops that can relay messages.'**
|
||||
String get path_customPathsRequire;
|
||||
|
||||
/// No description provided for @path_invalidHexPrefixes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid hex prefixes: {prefixes}'**
|
||||
String path_invalidHexPrefixes(String prefixes);
|
||||
|
||||
/// No description provided for @path_tooLong.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path too long. Maximum 64 hops allowed.'**
|
||||
String get path_tooLong;
|
||||
|
||||
/// No description provided for @path_setPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set Path'**
|
||||
String get path_setPath;
|
||||
|
||||
/// No description provided for @repeater_management.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3891,24 +3981,6 @@ abstract class AppLocalizations {
|
||||
/// **'Routing mode'**
|
||||
String get repeater_routingMode;
|
||||
|
||||
/// No description provided for @repeater_autoUseSavedPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto (use saved path)'**
|
||||
String get repeater_autoUseSavedPath;
|
||||
|
||||
/// No description provided for @repeater_forceFloodMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Force Flood Mode'**
|
||||
String get repeater_forceFloodMode;
|
||||
|
||||
/// No description provided for @repeater_pathManagement.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path management'**
|
||||
String get repeater_pathManagement;
|
||||
|
||||
/// No description provided for @repeater_refresh.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -7587,6 +7659,48 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Unknown'**
|
||||
String get contact_typeUnknown;
|
||||
|
||||
/// No description provided for @map_zoomIn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Zoom in'**
|
||||
String get map_zoomIn;
|
||||
|
||||
/// No description provided for @map_zoomOut.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Zoom out'**
|
||||
String get map_zoomOut;
|
||||
|
||||
/// No description provided for @map_centerMap.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Center map'**
|
||||
String get map_centerMap;
|
||||
|
||||
/// No description provided for @chrome_bluetoothRequiresChromium.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Web Bluetooth requires a Chromium browser'**
|
||||
String get chrome_bluetoothRequiresChromium;
|
||||
|
||||
/// No description provided for @channels_communityShortId.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'ID: {id}...'**
|
||||
String channels_communityShortId(String id);
|
||||
|
||||
/// No description provided for @pathTrace_legendGpsConfirmed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'GPS confirmed'**
|
||||
String get pathTrace_legendGpsConfirmed;
|
||||
|
||||
/// No description provided for @pathTrace_legendInferred.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Inferred position'**
|
||||
String get pathTrace_legendInferred;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
+177
-137
@@ -92,6 +92,24 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Деактивирай';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Рестартирай';
|
||||
|
||||
@@ -796,11 +814,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Батерия';
|
||||
|
||||
@@ -1002,6 +1015,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Нова група';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Група';
|
||||
|
||||
@@ -1482,35 +1504,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Хексадесетичен Dump:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Управление на пътища';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Покажи всички пътища';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Режим на маршрутизиране';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Автоматично (използвай запазения път)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Принуди режим на наводняване';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Неотдавни ACK пътища (докоснете, за да използвате):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Историята на пътя е пълна. Премахнете записи, за да добавите нови.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'скочи';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'скоци';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1522,12 +1515,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Успехи';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Премахни пътя';
|
||||
|
||||
@@ -1535,52 +1522,144 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Действия по пътя:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Задайте персонализиран път';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Ръчно укажете маршрутен път';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Почисти Път';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Принуди преоткриване при следващо изпращане';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Пътят е почистен. Следващото съобщение ще открие маршрута отново.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Използвайте превключвателя за маршрутизиране в лентата на приложението.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Пълен път';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Пътят е зададен: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Запазено локално. Свържете се за синхронизиране.';
|
||||
@@ -2050,66 +2129,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Изчисти';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Текущ път: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Използване на $count $_temp0 път';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Въведете персонализиран път';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Текущ път';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'A1,F2,3C (всяка нода използва първия байт от публичния си ключ)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Пътеки (шестнадесетични префикси)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Изберете от контакти:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Няма намерени репетитори или сървъри на стаи.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Невалидни шестнадесетични префикси: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Пътят е твърде дълъг. Максимум 64 скока са разрешени.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Задайте път';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Управление на повторители';
|
||||
|
||||
@@ -2176,16 +2202,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Режим на маршрутизиране';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Автоматично (използвай запазения път)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Принуди режим на наводняване';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Управление на пътища';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Презареди';
|
||||
|
||||
@@ -4437,4 +4453,28 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-136
@@ -92,6 +92,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Deaktivieren';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Neustart';
|
||||
|
||||
@@ -792,11 +810,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Akku';
|
||||
|
||||
@@ -998,6 +1011,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Neue Gruppe';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Gruppenname';
|
||||
|
||||
@@ -1480,36 +1502,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Hex-Dump:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Pfadverwaltung';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Alle Pfade anzeigen';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routenmodus';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath =>
|
||||
'Automatisch (gespeicherten Pfad verwenden)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Flut-Modus erzwingen';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Aktuelle ACK-Pfade (antippen, um zu verwenden):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'Sprung';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'Sprünge';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1521,12 +1513,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Erfolgreich';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Pfad entfernen';
|
||||
|
||||
@@ -1534,51 +1520,144 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Pfadaktionen:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Lege benutzerdefinierten Pfad fest';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Manuellen Routenpfad festlegen';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Pfad zurücksetzen';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Verwende den Routingschalter in der App-Leiste';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled => 'Flutmodus aktiviert.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Vollständiger Pfad';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Pfad gesetzt: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.';
|
||||
@@ -2049,65 +2128,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Löschen';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Aktiver Pfad: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Hops',
|
||||
one: 'Hop',
|
||||
);
|
||||
return 'Verwenden Sie $count $_temp0 Pfad';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Gebe Pfad ein';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Aktueller Pfad';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Pfad (Hex-Präfixe)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Oder wähle aus Kontakten aus:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Keine Repeater oder Raumserver gefunden.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Ungültige Hexadezimal-Präfixe: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => 'Pfad zu lang. Maximal 64 Hops erlaubt.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Pfad festlegen';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Repeater-Verwaltung';
|
||||
|
||||
@@ -2172,16 +2199,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Routenmodus';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Automatisch (gespeicherten Pfad verwenden)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Flut-Modus erzwingen';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Pfadverwaltung';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Aktualisieren';
|
||||
|
||||
@@ -4455,4 +4472,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-132
@@ -92,6 +92,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Disable';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Reboot';
|
||||
|
||||
@@ -777,11 +795,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Number of retry attempts before marking a message as failed';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Battery';
|
||||
|
||||
@@ -981,6 +994,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'New Group';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Group name';
|
||||
|
||||
@@ -1452,34 +1474,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Hex Dump:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Path Management';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Show all paths';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routing mode';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Auto (use saved path)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Force Flood Mode';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Recent ACK Paths (tap to use):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Path history is full. Remove entries to add new ones.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'hop';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'hops';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1491,12 +1485,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'successes';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Remove path';
|
||||
|
||||
@@ -1504,50 +1492,144 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'No path history yet.\nSend a message to discover paths.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Path Actions:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Set Custom Path';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Manually specify routing path';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Clear Path';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle => 'Force rediscovery on next send';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Path cleared. Next message will rediscover route.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle => 'Use routing toggle in app bar';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Flood mode enabled. Toggle back via routing icon in app bar.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Full Path';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Path details not available yet. Try sending a message to refresh.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Path set: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally => 'Saved locally. Connect to sync.';
|
||||
|
||||
@@ -2009,64 +2091,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Clear';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Current path: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Using $count $_temp0 path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Enter Custom Path';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Current path';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Enter 2-character hex prefixes for each hop, separated by commas.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Example: A1,F2,3C (each node uses first byte of its public key)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Path (hex prefixes)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Max 64 hops. Each prefix is 2 hex characters (1 byte)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Or select from contacts:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound => 'No repeaters or room servers found.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Custom paths require intermediate hops that can relay messages.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Invalid hex prefixes: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => 'Path too long. Maximum 64 hops allowed.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Set Path';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Repeater Management';
|
||||
|
||||
@@ -2130,15 +2160,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Routing mode';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Auto (use saved path)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Force Flood Mode';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Path management';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Refresh';
|
||||
|
||||
@@ -4360,4 +4381,28 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-136
@@ -92,6 +92,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Desactivar';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Reiniciar';
|
||||
|
||||
@@ -791,11 +809,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Número de intentos de reintento antes de marcar un mensaje como fallido.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batería';
|
||||
|
||||
@@ -997,6 +1010,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Nuevo Grupo';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Nombre del grupo';
|
||||
|
||||
@@ -1479,34 +1501,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Mapeo Hexadecimal:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestión de Rutas';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modo de enrutamiento';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Auto (usar la ruta guardada)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Modo Inundación Forzado';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Rutas de ACK Recientes (tocar para usar):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'El historial de rutas está completo. Eliminar entradas para añadir nuevas.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'salta';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'salta';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1518,12 +1512,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Éxitos';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Eliminar ruta';
|
||||
|
||||
@@ -1531,53 +1519,144 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Acciones de Ruta:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Establecer Ruta Personalizada';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle =>
|
||||
'Especificar manualmente la ruta de enrutamiento';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Limpiar Ruta';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Forzar redescubrimiento en el próximo envío';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Utilizar el interruptor de enrutamiento en la barra de herramientas';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Ruta completa';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Ruta establecida: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Guardado localmente. Conéctate para sincronizar.';
|
||||
@@ -2045,66 +2124,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Borrar';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Ruta actual: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Usando $count $_temp0 ruta';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Introducir Ruta Personalizada';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Ruta actual';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Introduzca los prefijos hexadecimales de 2 caracteres para cada salto, separados por comas.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Prefijos hexadecimales';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'O seleccionar de contactos:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'No se encontraron repetidores ni servidores de sala.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Las rutas personalizadas requieren saltos intermedios que pueden transmitir mensajes.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Prefijos hexadecimales inválidos: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'La ruta es demasiado larga. Se permiten un máximo de 64 saltos.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Establecer Ruta';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Gestión de Repetidores';
|
||||
|
||||
@@ -2169,15 +2195,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Modo de enrutamiento';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Auto (usar la ruta guardada)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Modo Inundación Forzado';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Gestión de rutas';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Actualizar';
|
||||
|
||||
@@ -4442,4 +4459,28 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-138
@@ -92,6 +92,24 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Désactiver';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Redémarrer';
|
||||
|
||||
@@ -797,11 +815,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batterie';
|
||||
|
||||
@@ -1003,6 +1016,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Nouveau Groupe';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Nom du groupe';
|
||||
|
||||
@@ -1485,35 +1507,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestion des chemins';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Afficher tous les chemins';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Mode de routage';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Mode tout le réseau forcé';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Chemins ACK récents (touchez pour utiliser) :';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'saut';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'sauts';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1525,12 +1518,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Succès';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Supprimer le chemin';
|
||||
|
||||
@@ -1538,53 +1525,144 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Actions du chemin :';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Définir un chemin personnalisé';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle =>
|
||||
'Spécifier manuellement le chemin de routage';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Effacer le chemin';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Forcer la redécouverte lors de la prochaine envoi';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Le chemin est dégagé. Le prochain message redécouvrira le tracé.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Désactive l\'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d\'application pour rebasculer en mode auto par la suite.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Chemin complet';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Chemin défini : $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Sauvegardé localement. Connectez-vous pour synchroniser.';
|
||||
@@ -2056,66 +2134,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Effacer';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Chemin actuel : $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Utiliser $count $_temp0 chemin';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Entrer un chemin personnalisé';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Chemin actuel';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Préfixes hexadécimaux';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Sélectionner à partir des contacts :';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Aucun répéteur ou room server n\'a été trouvé.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Préfixes hexadécimaux invalides : $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Le chemin est trop long. Maximum 64 sauts autorisés.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Définir le chemin';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Gestion des répéteurs';
|
||||
|
||||
@@ -2181,16 +2206,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Mode de routage';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Auto (utiliser le chemin sauvegardé)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Mode tout le réseau forcé';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Gestion des chemins';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Rafraîchir';
|
||||
|
||||
@@ -4471,4 +4486,28 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-139
@@ -92,6 +92,24 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Leteteszt';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Újraindítás';
|
||||
|
||||
@@ -795,11 +813,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Akku';
|
||||
|
||||
@@ -1003,6 +1016,15 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Új csoport';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Csoport neve';
|
||||
|
||||
@@ -1488,36 +1510,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Hex-dump:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Útvonal-kezelés';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mutasson meg minden útvonalat';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Útvonal-kezelési mód';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath =>
|
||||
'Automatikus (az eddigi útvonal használata)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Erőforrás-alapú áramlás mód';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Legutóbbi használt útvonalak (gombra kattintva):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Az előző lépések listája teljes. Törölj ki a bejegyzéseket, hogy újokat hozzáadhatsd.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'ugor';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'babér';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1529,12 +1521,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'sikerek';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Törölje a elérési útvonalat';
|
||||
|
||||
@@ -1542,52 +1528,144 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Céltúrások:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Beállítsd a saját útvonalat';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Kézzel megadott útvonal';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Egyértelmű út';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'A parancs új küldéskor újra kell aktivizálnia.';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Használja a \"útvonal\" kapcsolót az alkalmazás sávjában.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Árvízvédelmi mód bekapcsolva. A visszaállítás a alkalmazásban található útvonal ikon segítségével.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Teljes elérési út';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Az útvonal részletei még nem elérhetők. Próbálja meg küldeni egy üzenetet, hogy frissítse az információkat.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Path set: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.';
|
||||
@@ -2058,67 +2136,13 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Egyértelmű';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Jelenlegi útvonal: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'ugrások',
|
||||
one: 'ugrás',
|
||||
);
|
||||
return '$count $_temp0 útvonal használata';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Adja meg a saját elérési útvonalat';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Jelenlegi útvonal';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Adja meg a 2 karakteres hexadecimális előtagokat minden lépéshez, tagolva kommával.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Példa: A1, F2, 3C (minden csomó az első részét használja a nyilvános kulcsából)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Út (hex-prefixek)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'A maximális hossz 64 karakter. Minden előző rész 2 hatos számjegyből áll (1 bájt).';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts =>
|
||||
'Válasszon a kontaktlista elembek közül:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Az egyedi útvonalaknak szükségük van átjáró pontokra, amelyek képesek üzeneteket továbbítani.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Érvénytelen hexadecimális előtagok: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Az út túl hosszú. A maximális engedélyezett lépések száma 64.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Útvonal meghatározása';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Adatkapcsolás kezelése';
|
||||
|
||||
@@ -2184,16 +2208,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Útvonal-kezelési mód';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Automatikus (az eddigi útvonal használata)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Erőforrás-alapú áramlás mód';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Útvonal-kezelés';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Újrafriszol';
|
||||
|
||||
@@ -4458,4 +4472,28 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-136
@@ -92,6 +92,24 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Disattivare';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Riavvia';
|
||||
|
||||
@@ -794,11 +812,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batteria';
|
||||
|
||||
@@ -999,6 +1012,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Nuovo Gruppo';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Nome gruppo';
|
||||
|
||||
@@ -1481,34 +1503,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Dumpa Esadecimale:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestione Percorsi';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modalità di routing';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Utilizza il percorso salvato';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'salta';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'salta';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1520,12 +1514,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'successi';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Rimuovi percorso';
|
||||
|
||||
@@ -1533,53 +1521,144 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Azioni Percorso:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Imposta Percorso Personalizzato';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle =>
|
||||
'Specifica manualmente il percorso di routing';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Cancella Percorso';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Riprova la scoperta alla prossima invio';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Utilizza l\'interruttore di routing nella barra delle applicazioni';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Percorso Completo';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Percorso impostato: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Salvatato localmente. Connetti per sincronizzare.';
|
||||
@@ -2047,66 +2126,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Cancella';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Percorso corrente: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Utilizzare $count $_temp0 percorso';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Inserisci percorso personalizzato';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Percorso corrente';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Prefisso esadecimale (percorso)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Seleziona da contatti:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Non sono stati trovati ripetitori o server di stanza.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Prefissi esadecimali non validi: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Il percorso è troppo lungo. Massimo 64 salti consentiti.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Imposta Percorso';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Gestione Ripetitori';
|
||||
|
||||
@@ -2173,15 +2199,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Modalità di routing';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Percorso salvato automatico';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Modalità Inondamento Forzato';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Gestione dei percorsi';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Aggiorna';
|
||||
|
||||
@@ -4447,4 +4464,28 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+247
-199
@@ -92,6 +92,24 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => '無効化する';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => '再起動';
|
||||
|
||||
@@ -559,7 +577,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => '無効な送信電力 (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeat => 'オフグリッド(電力網から孤立した状態)の繰り返し';
|
||||
String get settings_clientRepeat => 'オフグリッドリピータ';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
@@ -709,7 +727,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotation => '自動ルートの切り替え';
|
||||
|
||||
@override
|
||||
String get appSettings_autoRouteRotationSubtitle => '最適なルートと、洪水モードを切り替える';
|
||||
String get appSettings_autoRouteRotationSubtitle => '最適なルートと、フラッドモードを切り替える';
|
||||
|
||||
@override
|
||||
String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています';
|
||||
@@ -751,11 +769,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'メッセージを「失敗」とマークするまでの、再試行回数';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'バッテリー';
|
||||
|
||||
@@ -786,7 +799,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get appSettings_showRepeaters => '繰り返し再生機能';
|
||||
|
||||
@override
|
||||
String get appSettings_showRepeatersSubtitle => '地図上にリピーターノードを表示する';
|
||||
String get appSettings_showRepeatersSubtitle => '地図上にリピータノードを表示する';
|
||||
|
||||
@override
|
||||
String get appSettings_showChatNodes => 'チャットノードの表示';
|
||||
@@ -925,7 +938,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_manageRepeater => 'リピーターの管理';
|
||||
String get contacts_manageRepeater => 'リピータの管理';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'ルームサーバーの管理';
|
||||
@@ -950,6 +963,15 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => '新しいグループ';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'グループ名';
|
||||
|
||||
@@ -1416,33 +1438,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'ヘックスダンプ:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => '経路管理';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'すべての経路を表示';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'ルーティングモード';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => '自動 (保存されたパスを使用)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => '強制的に洪水モードを起動';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => '最近使用したACKパス(タップして使用):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull => 'パスの履歴は完全です。エントリを削除して、新しいものを追加できます。';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'ジャンプ';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'ホップ';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1454,61 +1449,149 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => '成功事例';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'パスを削除する';
|
||||
|
||||
@override
|
||||
String get chat_noPathHistoryYet => 'まだ履歴はありません。\nパスを特定するためにメッセージを送信してください。';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'パスの操作:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'カスタムパスを設定';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => '手動で経路を指定する';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => '明確な道';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle => '次回送信時に、以前の情報を再取得する';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared => '経路が確保されました。次のメッセージでルートを再確認します。';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle => 'アプリのバーにあるルーティング切り替え機能を使用する';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'洪水モードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'フルパス';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Path set: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally => 'ローカルで保存。同期のために接続する。';
|
||||
|
||||
@@ -1531,7 +1614,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する';
|
||||
|
||||
@override
|
||||
String get chat_floodForced => '洪水(強制的な)';
|
||||
String get chat_floodForced => 'フラッド(強制的な)';
|
||||
|
||||
@override
|
||||
String get chat_directForced => '直接的な(強制的な)';
|
||||
@@ -1542,7 +1625,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_floodAuto => '洪水 (自動)';
|
||||
String get chat_floodAuto => 'フラッド (自動)';
|
||||
|
||||
@override
|
||||
String get chat_direct => '直接';
|
||||
@@ -1607,7 +1690,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get map_chat => 'チャット';
|
||||
|
||||
@override
|
||||
String get map_repeater => '繰り返し送信装置';
|
||||
String get map_repeater => 'リピータ';
|
||||
|
||||
@override
|
||||
String get map_room => '部屋';
|
||||
@@ -1702,13 +1785,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get map_chatNodes => 'チャットノード';
|
||||
|
||||
@override
|
||||
String get map_repeaters => '繰り返し送信装置';
|
||||
String get map_repeaters => 'リピータ';
|
||||
|
||||
@override
|
||||
String get map_otherNodes => 'その他のノード';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'リピーターキーの重複';
|
||||
String get map_showOverlaps => 'リピータキーの重複';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => '主要なプレフィックス';
|
||||
@@ -1747,7 +1830,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get map_joinRoom => '部屋に参加する';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'リピーターの管理';
|
||||
String get map_manageRepeater => 'リピータの管理';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'ノードをクリックして、パスに追加します。';
|
||||
@@ -1920,7 +2003,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get login_repeaterDescription =>
|
||||
'設定やステータスにアクセスするために、リピーターのパスワードを入力してください。';
|
||||
'設定やステータスにアクセスするために、リピータのパスワードを入力してください。';
|
||||
|
||||
@override
|
||||
String get login_roomDescription => '設定やステータスへのアクセスには、部屋のパスワードを入力してください。';
|
||||
@@ -1935,7 +2018,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get login_autoUseSavedPath => '自動 (保存されたパスを使用)';
|
||||
|
||||
@override
|
||||
String get login_forceFloodMode => '強制的に洪水モードを起動';
|
||||
String get login_forceFloodMode => '強制的にフラッドモードを起動';
|
||||
|
||||
@override
|
||||
String get login_managePaths => 'パスの管理';
|
||||
@@ -1963,70 +2046,20 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => '明確';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return '現在のパス: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'ホップ',
|
||||
one: 'ホップ',
|
||||
);
|
||||
return '$count $_temp0のパスを使用';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'カスタムパスを入力';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => '現在の経路';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'各ホップに対して、2文字の16進数プレフィックスをカンマで区切って入力してください。';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample => '例:A1, F2, 3C (各ノードは、自身の公開鍵の最初のバイトを使用)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'パス (ヘックスプレフィックス)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'最大64個のホップ。各プレフィックスは2つの16進数文字(1バイト)で構成されています。';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'または、連絡先リストから選択してください:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound => '繰り返し機能やルームサーバーは見つかりませんでした。';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire => 'カスタムパスには、メッセージを中継できる中間地点が必要です。';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return '無効な16進数プレフィックス: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => '経路が長すぎる。最大64回のジャンプのみ許可。';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'パスを設定';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'リピーター管理';
|
||||
String get repeater_management => 'リピータ管理';
|
||||
|
||||
@override
|
||||
String get room_management => 'ルームサーバーの管理';
|
||||
|
||||
@override
|
||||
String get repeater_guest => '繰り返し送信に関する情報';
|
||||
String get repeater_guest => 'リピータに関する情報';
|
||||
|
||||
@override
|
||||
String get room_guest => 'ルームサーバーに関する情報';
|
||||
@@ -2041,7 +2074,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_status => 'ステータス';
|
||||
|
||||
@override
|
||||
String get repeater_statusSubtitle => 'リピーターの状態、統計情報、および隣接するネットワークの情報を表示する';
|
||||
String get repeater_statusSubtitle => 'リピータの状態、統計情報、および隣接するネットワークの情報を表示する';
|
||||
|
||||
@override
|
||||
String get repeater_telemetry => 'テレメトリー';
|
||||
@@ -2053,7 +2086,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_cli => 'CLI(コマンドラインインターフェース)';
|
||||
|
||||
@override
|
||||
String get repeater_cliSubtitle => 'リピーターへのコマンドを送信する';
|
||||
String get repeater_cliSubtitle => 'リピータへのコマンドを送信する';
|
||||
|
||||
@override
|
||||
String get repeater_neighbors => '近隣住民';
|
||||
@@ -2065,7 +2098,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_settings => '設定';
|
||||
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する';
|
||||
String get repeater_settingsSubtitle => 'リピータのパラメータを設定する';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する';
|
||||
@@ -2080,15 +2113,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'ルーティングモード';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => '自動 (保存されたパスを使用)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => '強制的に洪水モードを起動';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => '経路管理';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'リフレッシュ';
|
||||
|
||||
@@ -2163,12 +2187,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String repeater_packetTxTotal(int total, String flood, String direct) {
|
||||
return '合計: $total, 洪水: $flood, 直接: $direct';
|
||||
return '合計: $total, フラッド: $flood, 直接: $direct';
|
||||
}
|
||||
|
||||
@override
|
||||
String repeater_packetRxTotal(int total, String flood, String direct) {
|
||||
return '合計: $total, 洪水: $flood, 直接: $direct';
|
||||
return '合計: $total, フラッド: $flood, 直接: $direct';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2182,16 +2206,16 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_settingsTitle => 'リピーター設定';
|
||||
String get repeater_settingsTitle => 'リピータ設定';
|
||||
|
||||
@override
|
||||
String get repeater_basicSettings => '基本設定';
|
||||
|
||||
@override
|
||||
String get repeater_repeaterName => '送信装置名';
|
||||
String get repeater_repeaterName => 'リピータ名';
|
||||
|
||||
@override
|
||||
String get repeater_repeaterNameHelper => 'このリピーターの名前';
|
||||
String get repeater_repeaterNameHelper => 'このリピータの名前';
|
||||
|
||||
@override
|
||||
String get repeater_adminPassword => '管理者パスワード';
|
||||
@@ -2251,7 +2275,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_packetForwarding => 'パケット転送';
|
||||
|
||||
@override
|
||||
String get repeater_packetForwardingSubtitle => 'リピーターがパケットを転送できるように設定する';
|
||||
String get repeater_packetForwardingSubtitle => 'リピータがパケットを転送できるように設定する';
|
||||
|
||||
@override
|
||||
String get repeater_guestAccess => 'ゲストへのアクセス';
|
||||
@@ -2277,7 +2301,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_floodAdvertInterval => '洪水に関する広告の表示間隔';
|
||||
String get repeater_floodAdvertInterval => 'フラッドに関する広告の表示間隔';
|
||||
|
||||
@override
|
||||
String repeater_floodAdvertIntervalHours(int hours) {
|
||||
@@ -2291,13 +2315,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_dangerZone => '危険区域';
|
||||
|
||||
@override
|
||||
String get repeater_rebootRepeater => 'リピーターを再起動する';
|
||||
String get repeater_rebootRepeater => 'リピータを再起動する';
|
||||
|
||||
@override
|
||||
String get repeater_rebootRepeaterSubtitle => 'リピーターデバイスを再起動する';
|
||||
String get repeater_rebootRepeaterSubtitle => 'リピータデバイスを再起動する';
|
||||
|
||||
@override
|
||||
String get repeater_rebootRepeaterConfirm => '本当にこのリピーターを再起動したいですか?';
|
||||
String get repeater_rebootRepeaterConfirm => '本当にこのリピータを再起動したいですか?';
|
||||
|
||||
@override
|
||||
String get repeater_regenerateIdentityKey => 'IDキーの再生成';
|
||||
@@ -2307,17 +2331,17 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_regenerateIdentityKeyConfirm =>
|
||||
'これにより、リピーターには新しい識別情報が割り当てられます。続行しますか?';
|
||||
'これにより、リピータには新しい識別情報が割り当てられます。続行しますか?';
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystem => 'ファイルシステムを削除する';
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystemSubtitle => 'リピーターファイルシステムをフォーマットする';
|
||||
String get repeater_eraseFileSystemSubtitle => 'リピータファイルシステムをフォーマットする';
|
||||
|
||||
@override
|
||||
String get repeater_eraseFileSystemConfirm =>
|
||||
'警告:この操作により、リピーター内のすべてのデータが消去されます。この操作は元に戻すことができません!';
|
||||
'警告:この操作により、リピータ内のすべてのデータが消去されます。この操作は元に戻すことができません!';
|
||||
|
||||
@override
|
||||
String get repeater_eraseSerialOnly => 'Erase機能は、シリアルコンソール経由でのみ利用可能です。';
|
||||
@@ -2363,7 +2387,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_loopDetect => 'ループ検出';
|
||||
|
||||
@override
|
||||
String get repeater_loopDetectHelper => 'ルーティングループに見えるような、洪水パケットを送信する';
|
||||
String get repeater_loopDetectHelper => 'ルーティングループを検知する';
|
||||
|
||||
@override
|
||||
String get repeater_loopDetectOff => 'オフ';
|
||||
@@ -2392,7 +2416,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_ownerInfo => '事業者の情報';
|
||||
|
||||
@override
|
||||
String get repeater_ownerInfoHelper => 'このリピーターに関する公開メタデータ';
|
||||
String get repeater_ownerInfoHelper => 'このリピータに関する公開メタデータ';
|
||||
|
||||
@override
|
||||
String get repeater_refreshOwnerInfo => 'オペレーター情報の更新';
|
||||
@@ -2401,7 +2425,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_floodMax => '最大ホップ数';
|
||||
|
||||
@override
|
||||
String get repeater_floodMaxHelper => '洪水パケットが移動できる最大ホップ数 (0-64)';
|
||||
String get repeater_floodMaxHelper => 'フラッドパケットが移動できる最大ホップ数 (0-64)';
|
||||
|
||||
@override
|
||||
String get repeater_advancedSettings => '高度な';
|
||||
@@ -2414,14 +2438,14 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'このリピーターのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。';
|
||||
'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。';
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'フロイド・TXでの遅延';
|
||||
|
||||
@override
|
||||
String get repeater_txDelayHelper =>
|
||||
'洪水時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。';
|
||||
'フラッド時の交通量に対応するための再送信間隔を、パケットの通信時間を掛けた値(0~2、デフォルト0.5)で設定します。値を大きくすると衝突が減りますが、通信速度が遅くなります。';
|
||||
|
||||
@override
|
||||
String get repeater_directTxDelay => '直接的なTX遅延';
|
||||
@@ -2448,16 +2472,16 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_actionsTitle => '行動';
|
||||
|
||||
@override
|
||||
String get repeater_sendAdvert => '洪水に関する広告を送信';
|
||||
String get repeater_sendAdvert => 'フラッドに関する広告を送信';
|
||||
|
||||
@override
|
||||
String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、洪水に関する広告を放送する';
|
||||
String get repeater_sendAdvertSubtitle => 'ネットワークを通じて、フラッドに関する広告を放送する';
|
||||
|
||||
@override
|
||||
String get repeater_sendAdvertZeroHop => 'ゼロホップ形式の広告を送信する';
|
||||
|
||||
@override
|
||||
String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(中継なし)';
|
||||
String get repeater_sendAdvertZeroHopSubtitle => 'ワンホップでの広告放送(リピータなし)';
|
||||
|
||||
@override
|
||||
String get repeater_clockSync => '現在、時刻を同期する';
|
||||
@@ -2477,7 +2501,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_settingsSavedRebootNeeded =>
|
||||
'設定を保存しました — リピーターを再起動して適用してください';
|
||||
'設定を保存しました — リピータを再起動して適用してください';
|
||||
|
||||
@override
|
||||
String repeater_settingsPartialFailure(String failures) {
|
||||
@@ -2518,7 +2542,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_cliTitle => 'リピーターのコマンドラインインターフェース';
|
||||
String get repeater_cliTitle => 'リピータのコマンドラインインターフェース';
|
||||
|
||||
@override
|
||||
String get repeater_debugNextCommand => '次のコマンドのデバッグ';
|
||||
@@ -2609,7 +2633,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_cliHelpSetTx => 'LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetRepeat => 'このノードに対するリピーターの役割を有効化または無効化します。';
|
||||
String get repeater_cliHelpSetRepeat => 'このノードに対するリピータの役割を有効化または無効化します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetAllowReadOnly =>
|
||||
@@ -2617,7 +2641,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetFloodMax =>
|
||||
'インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。';
|
||||
'インバウンドフラッドパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetIntThresh =>
|
||||
@@ -2636,7 +2660,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetFloodAdvertInterval =>
|
||||
'洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。';
|
||||
'フラッド広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetGuestPassword =>
|
||||
@@ -2710,7 +2734,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpNeighbors =>
|
||||
'ゼロホップ広告を通じて受信した他のリピーターノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。';
|
||||
'ゼロホップ広告を通じて受信した他のリピータノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpNeighborRemove =>
|
||||
@@ -2718,7 +2742,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegion =>
|
||||
'(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。';
|
||||
'(特定のシリーズのみ)定義されたすべての地域と、現在のフラッド許可状況を一覧表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionLoad =>
|
||||
@@ -2737,7 +2761,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionAllowf =>
|
||||
'指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)';
|
||||
'指定された領域に対して、「フラッド」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionDenyf =>
|
||||
@@ -2793,7 +2817,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_logging => 'ログ記録';
|
||||
|
||||
@override
|
||||
String get repeater_neighborsRepeaterOnly => '近隣住民(リピーターのみ)';
|
||||
String get repeater_neighborsRepeaterOnly => '近隣住民(リピータのみ)';
|
||||
|
||||
@override
|
||||
String get repeater_regionManagementRepeaterOnly => '地域管理(ブロードキャスト用のみ)';
|
||||
@@ -2838,7 +2862,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpDiscoverNeighbors =>
|
||||
'近隣のノードに対して、ノードの探索リクエストを送信します。(中継機能のみ)';
|
||||
'近隣のノードに対して、ノードの探索リクエストを送信します。(リピータ機能のみ)';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpPowersaving => '省電力モードがオンになっているかどうかを表示します。';
|
||||
@@ -2887,7 +2911,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_cliHelpGetName => '設定されたノードの名前を表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピーター、ルームサーバーなど)を表示します。';
|
||||
String get repeater_cliHelpGetRole => 'ファームウェアの役割(リピータ、ルームサーバーなど)を表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetPublicKey => 'デバイスの公開鍵を表示します。';
|
||||
@@ -2897,7 +2921,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
'(シリアル番号のみ)デバイスのプライベートキーを表示します。機密情報として扱ってください。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetRepeat => 'パケット転送(リピーター機能)が有効になっているかどうかを表示します。';
|
||||
String get repeater_cliHelpGetRepeat => 'パケット転送(リピータ機能)が有効になっているかどうかを表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetTx => '現在のTX(送信)電力のdBm値を表示します。';
|
||||
@@ -2936,7 +2960,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetFloodAdvertInterval =>
|
||||
'洪水に関する広告の放送時間を時間単位で表示します。';
|
||||
'フラッドに関する広告の放送時間を時間単位で表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetGuestPassword => '設定されたゲストパスワードを表示します。';
|
||||
@@ -2951,13 +2975,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_cliHelpGetRxDelay => 'rxdelay の基本値を表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetTxDelay => '洪水モードにおける送信遅延の要因を示します。';
|
||||
String get repeater_cliHelpGetTxDelay => 'フラッドモードにおける送信遅延の要因を示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetDirectTxDelay => 'ダイレクトモードの遅延要素を示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetFloodMax => '最大浸水範囲の回数を表示します。';
|
||||
String get repeater_cliHelpGetFloodMax => 'フラッドパケットの最大ホップ数を表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetOwnerInfo => '所有者の連絡先情報を表示します。';
|
||||
@@ -2969,7 +2993,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_cliHelpGetLoopDetect => 'ループ検出の感度を示す。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetAcl => '(シリアルのみ)リピーター上のアクセス制御設定を一覧表示します。';
|
||||
String get repeater_cliHelpGetAcl => '(シリアルのみ)リピータ上のアクセス制御設定を一覧表示します。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpGetBridgeEnabled => '橋が有効になっているかどうかを表示します。';
|
||||
@@ -3032,10 +3056,10 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
'デフォルトの地域範囲を設定します。「<null>」を使用すると、設定をリセットできます。';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionListAllowed => '洪水時の通行が許可されている地域の一覧';
|
||||
String get repeater_cliHelpRegionListAllowed => 'フラッド時の通行が許可されている地域の一覧';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpRegionListDenied => '洪水による交通を遮断している地域の一覧';
|
||||
String get repeater_cliHelpRegionListDenied => 'フラッドによる交通を遮断している地域の一覧';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpStatsPackets => '(シリアルのみ)パケットレベルの統計情報を表示します。';
|
||||
@@ -3245,7 +3269,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbors => '繰り返し送信する、近隣';
|
||||
String get neighbors_repeatersNeighbors => '近隣のリピータ';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => '近隣のデータは利用できません。';
|
||||
@@ -3270,7 +3294,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get channelPath_otherObservedPaths => '観察されたその他の経路';
|
||||
|
||||
@override
|
||||
String get channelPath_repeaterHops => 'ホップの繰り返し';
|
||||
String get channelPath_repeaterHops => 'リピータホップ';
|
||||
|
||||
@override
|
||||
String get channelPath_noHopDetails => 'このパッケージに関する詳細な情報は提供されていません。';
|
||||
@@ -3285,7 +3309,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get channelPath_timeLabel => '時間';
|
||||
|
||||
@override
|
||||
String get channelPath_repeatsLabel => '繰り返し';
|
||||
String get channelPath_repeatsLabel => 'リピータ';
|
||||
|
||||
@override
|
||||
String channelPath_pathLabel(int index) {
|
||||
@@ -3317,7 +3341,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get channelPath_unknownPath => '不明';
|
||||
|
||||
@override
|
||||
String get channelPath_floodPath => '洪水';
|
||||
String get channelPath_floodPath => 'フラッド';
|
||||
|
||||
@override
|
||||
String get channelPath_directPath => '直接';
|
||||
@@ -3336,7 +3360,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get channelPath_mapTitle => '経路図';
|
||||
|
||||
@override
|
||||
String get channelPath_noRepeaterLocations => 'この経路には、中継装置の設置場所がありません。';
|
||||
String get channelPath_noRepeaterLocations => 'この経路にリピータの位置情報はありません。';
|
||||
|
||||
@override
|
||||
String channelPath_primaryPath(int index) {
|
||||
@@ -3556,7 +3580,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get listFilter_users => '利用者';
|
||||
|
||||
@override
|
||||
String get listFilter_repeaters => '繰り返し送信装置';
|
||||
String get listFilter_repeaters => 'リピータ';
|
||||
|
||||
@override
|
||||
String get listFilter_roomServers => 'ルーム用サーバー';
|
||||
@@ -3756,10 +3780,10 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get contacts_ping => 'パング';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'リピーターまでの経路を追跡する';
|
||||
String get contacts_repeaterPathTrace => 'リピータまでの経路を追跡する';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'PING 繰り返し';
|
||||
String get contacts_repeaterPing => 'リピータにPING';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace => '部屋のサーバーへの経路を追跡する';
|
||||
@@ -3791,7 +3815,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get contacts_zeroHopAdvert => 'ゼロホップ広告';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => '洪水に関する広告';
|
||||
String get contacts_floodAdvert => 'フラッドに関する広告';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => '広告をクリップボードにコピー';
|
||||
@@ -3862,7 +3886,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get notification_receivedNewMessage => '新しいメッセージを受信';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピーター/ルームサーバー';
|
||||
String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピータ/ルームサーバー';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
@@ -3895,7 +3919,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get settings_gpxExportError => 'エクスポート時にエラーが発生しました。';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom => '中継装置およびルームサーバーの設置場所';
|
||||
String get settings_gpxExportRepeatersRoom => 'リピータ/ルームサーバーの位置情報';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => '関連施設';
|
||||
@@ -3911,7 +3935,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
'meshcore-open GPX形式の地図データのエクスポート';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => '近くの電波中継局';
|
||||
String get snrIndicator_nearByRepeaters => '近くのリピータ';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => '最後に確認された場所';
|
||||
@@ -3933,11 +3957,11 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
'利用者が自動的に発見したユーザーを追加できるようにする。';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle => '自動で繰り返し設定';
|
||||
String get contactsSettings_autoAddRepeatersTitle => 'リピータを自動追加';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'発見した中継局を、自動的に追加できるようにする。';
|
||||
'発見したリピータを、自動的に追加できるようにする。';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle => '自動でルームサーバーを追加';
|
||||
@@ -4212,4 +4236,28 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-132
@@ -92,6 +92,24 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => '비활성화';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => '재부팅';
|
||||
|
||||
@@ -749,11 +767,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle => '메시지를 실패로 처리하기 전 시도 횟수';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => '배터리';
|
||||
|
||||
@@ -945,6 +958,15 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => '새로운 그룹';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => '그룹 이름';
|
||||
|
||||
@@ -1411,34 +1433,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => '헥스 덤프:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => '경로 관리';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => '모든 경로 표시';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => '라우팅 방식';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => '자동 (저장된 경로 사용)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => '강수 모드 활성화';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => '최근 사용한 ACK 경로 (사용하려면 탭):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'이력 기록은 이미 가득 차 있습니다. 항목을 삭제하여 새로운 항목을 추가할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => '점프';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => '홉';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1450,61 +1444,149 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => '성공 사례';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => '경로 제거';
|
||||
|
||||
@override
|
||||
String get chat_noPathHistoryYet => '아직 경로 기록이 없습니다.\n경로를 찾기 위해 메시지를 보내세요.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => '경로 작업:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => '사용자 지정 경로 설정';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => '수동으로 경로를 지정';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => '명확한 길';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle => '다음 전송 시, 강제 재전송 설정';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared => '경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle => '앱 바에서 라우팅 스위치를 사용';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'홍수 모드 활성화됨. 앱 바의 경로 아이콘을 사용하여 다시 전환할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => '전체 경로';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'경로 정보는 아직 제공되지 않습니다. 메시지를 보내어 다시 시도해 보세요.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Path set: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결';
|
||||
|
||||
@@ -1958,64 +2040,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => '명확하게';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return '현재 경로: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Using $count $_temp0 path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => '사용자 지정 경로 입력';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => '현재 경로';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'각 단계에 대한 2자리 헥사데진 접두사를 쉼표로 구분하여 입력하세요.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'예시: A1, F2, 3C (각 노드는 자신의 공개 키의 첫 번째 바이트를 사용)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => '경로 (헥스 접두사)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'최대 64개의 홉. 각 접두사는 2개의 16진수 문자(1바이트)로 구성됩니다.';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => '또 연락처 목록에서 선택:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound => '반복 장치 또는 서버는 찾을 수 없습니다.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'사용자 정의 경로에는 메시지를 전달할 수 있는 중간 경로가 필요합니다.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return '유효하지 않은 16진수 접두사: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => '경로가 너무 길어. 최대 64개의 연결만 허용됩니다.';
|
||||
|
||||
@override
|
||||
String get path_setPath => '경로 설정';
|
||||
|
||||
@override
|
||||
String get repeater_management => '리피터 관리';
|
||||
|
||||
@@ -2077,15 +2107,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => '라우팅 방식';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => '자동 (저장된 경로 사용)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => '강수 모드 활성화';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => '경로 관리';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => '새롭게';
|
||||
|
||||
@@ -4213,4 +4234,28 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-136
@@ -92,6 +92,24 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Uitschakelen';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Herstarten';
|
||||
|
||||
@@ -786,11 +804,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batterij';
|
||||
|
||||
@@ -991,6 +1004,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Nieuwe Groep';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Groepnaam';
|
||||
|
||||
@@ -1468,34 +1490,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Hex-dump:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Beheer van Paden';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Toon alle paden';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routeerwijze';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Automatisch (gebruik opgeslagen pad)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Dwing Floodsmodus';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Recente ACK Paden (tik om te gebruiken):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'De voorgeschiedenis is vol. Verwijder vermeldingen om er nieuwe aan toe te voegen.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'Hop';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'hoppen';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1507,12 +1501,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Succesvol';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Pad verwijderen';
|
||||
|
||||
@@ -1520,52 +1508,144 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Geen geschiedenis van paden nog beschikbaar.\nVerzend een bericht om paden te ontdekken.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Padacties:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Stel aangepaste pad in';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Handmatig routepad specificeren';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Duidelijke Pad';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Dwing herontdekking bij volgende verzending';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Pad is vrijgegeven. Volgende bericht herontdekt route.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Gebruik de route-schakelaar in de app-balk';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Volledige Pad';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'De paddetails zijn nog niet beschikbaar. Probeer een bericht te sturen om te vernieuwen.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Pad ingesteld: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Opgeslagen lokaal. Verbinden om te synchroniseren.';
|
||||
@@ -2034,65 +2114,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Schoonmaken';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Huidige pad: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Gebruik $count $_temp0 pad';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Voer aangepaste pad in';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Huidige pad';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Voer 2-letter hex-voorgiffen voor elke hop in, gescheiden door komma\'s.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Voorbeeld: A1,F2,3C (elke node gebruikt het eerste byte van zijn openbare sleutel)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Pad (hex-voorkeursletters)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Maximaal 64 sprongen. Elke prefix is 2 hexadecimale tekens (1 byte)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Of select contacten:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound => 'Geen repeaters of roomservers gevonden.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Aangepaste paden vereisen tussentse overstappen die berichten kunnen doorgeven.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Ongeldige hex-voorkeursletters: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Pad is te lang. Maximaal 64 sprongen zijn toegestaan.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Stel Pad in';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Beheer Repeaters';
|
||||
|
||||
@@ -2157,16 +2184,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Routeerwijze';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Automatisch (gebruik opgeslagen pad)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Dwing Floodmodus Af';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Beheer van paden';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Vernieuwen';
|
||||
|
||||
@@ -4422,4 +4439,28 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-139
@@ -92,6 +92,24 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Wyłącz';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Uruchom ponownie';
|
||||
|
||||
@@ -796,11 +814,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Bateria';
|
||||
|
||||
@@ -1009,6 +1022,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Nowa Grupa';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Nazwa grupy';
|
||||
|
||||
@@ -1491,35 +1513,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Zrzut hex:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Zarządzanie ścieżkami';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Tryb routingu';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Automatyczne (użyj zapisanej ścieżki)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Wymuś tryb zalewowy';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Ostatnie ścieżki ACK (naciśnij, aby użyć):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'skok';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'skoki';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1533,12 +1526,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Sukcesy';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Usuń ścieżkę';
|
||||
|
||||
@@ -1546,52 +1533,144 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Brak historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Działania ścieżki:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Ustaw ścieżkę niestandardową';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Ręcznie określ trasę.';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Wyczyść Ścieżkę';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Wymuś ponowne wyznaczenie trasy przy następnym wysłaniu';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Ścieżka wyczyszczona. Następna wiadomość odnajdzie trasę.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Użyj przełącznika routingu w pasku narzędzi.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Tryb zalewowy włączony. Przełącz z powrotem ikoną routingu w pasku aplikacji.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Pełna ścieżka';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Ścieżka ustawiona: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Zapisano lokalnie. Połącz się, aby zsynchronizować.';
|
||||
@@ -2061,68 +2140,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Wyczyść';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Aktualna ścieżka: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'skoków',
|
||||
many: 'skoków',
|
||||
few: 'skoki',
|
||||
one: 'skok',
|
||||
);
|
||||
return 'Użyj ścieżki $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Wprowadź własną ścieżkę';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Aktualna ścieżka';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'A1,F2,3C (każdy węzeł używa pierwszego bajtu swojego klucza publicznego)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Ścieżka (prefiksy hex)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Maksymalnie 64 skoki. Każdy prefiks ma 2 znaki szesnastkowe (1 bajt).';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Albo wybierz z kontaktów:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Nie znaleziono przekaźników ani serwerów pokoi.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Nieprawidłowe prefiksy szesnastkowe: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Ustaw Ścieżkę';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Zarządzanie przekaźnikami';
|
||||
|
||||
@@ -2187,16 +2211,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Tryb routingu';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Automatycznie (użyj zapisanej ścieżki)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Wymuś tryb zalewowy';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Zarządzanie ścieżkami';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Odśwież';
|
||||
|
||||
@@ -4459,4 +4473,28 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-136
@@ -92,6 +92,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Desativar';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Reiniciar';
|
||||
|
||||
@@ -793,11 +811,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Número de tentativas de reenvio antes de classificar uma mensagem como falha.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Bateria';
|
||||
|
||||
@@ -999,6 +1012,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Novo Grupo';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Nome do grupo';
|
||||
|
||||
@@ -1478,34 +1500,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Espaço Hexadecimal:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Gerenciamento de Caminhos';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modo de roteamento';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Auto (usar caminho salvo)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Modo de Inundação Forçado';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Rotas de ACK Recentes (toque para usar):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'O histórico está cheio. Remova entradas para adicionar novas.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'pule';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'salta';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1517,12 +1511,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Sucessos';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Remover caminho';
|
||||
|
||||
@@ -1530,53 +1518,144 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Ações do Caminho:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Definir Caminho Personalizado';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle =>
|
||||
'Especifique manualmente o caminho de roteamento';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Limpar Caminho';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Forçar a descoberta na próxima transmissão';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Caminho limpo. A próxima mensagem redescobrirá a rota.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Use a chave de roteamento na barra de ferramentas';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Caminho Completo';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Caminho definido: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Salvo localmente. Conectar para sincronizar.';
|
||||
@@ -2044,66 +2123,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Limpar';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Caminho atual: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Usando $count $_temp0 caminho';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Insira Caminho Personalizado';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Caminho atual';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Prefixo Hexadecimal';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Ou selecione de contatos:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Não foram encontrados repetidores ou servidores de sala.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Prefixos hexadecimais inválidos: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Caminho muito longo. Máximo de 64 saltos permitidos.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Definir Caminho';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Gerenciamento de Repetidor';
|
||||
|
||||
@@ -2168,15 +2194,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Modo de roteamento';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Auto (usar caminho salvo)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Modo de Inundação Forçado';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Gerenciamento de caminhos';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Atualizar';
|
||||
|
||||
@@ -4435,4 +4452,28 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-140
@@ -92,6 +92,24 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Выключить';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Перезагрузить';
|
||||
|
||||
@@ -795,11 +813,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Батарея';
|
||||
|
||||
@@ -1000,6 +1013,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Новая группа';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Имя группы';
|
||||
|
||||
@@ -1479,35 +1501,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Шестнадцатеричный дамп:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Управление маршрутами';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Показать все пути';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Режим маршрутизации';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Принудительный режим рассылки';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Недавние подтверждённые маршруты (нажмите, чтобы использовать):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'История маршрутов заполнена. Удалите записи, чтобы добавить новые.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'хоп';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'хопов';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1521,12 +1514,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'успешно';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Оценка';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Удалить маршрут';
|
||||
|
||||
@@ -1534,54 +1521,144 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Действия с маршрутом:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Указать маршрут вручную';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Очистить маршрут';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Принудительно обновить маршрут при следующей отправке';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Маршрут очищен. Следующее сообщение обновит маршрут.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Используйте переключатель маршрутизации в панели приложения';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Полный маршрут';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'хопов',
|
||||
many: 'хопов',
|
||||
few: 'хопа',
|
||||
one: 'хоп',
|
||||
);
|
||||
return 'Маршрут установлен: $hopCount $_temp0 — $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Сохранено локально. Подключитесь для синхронизации.';
|
||||
@@ -2049,66 +2126,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Очистить';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Текущий маршрут: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'хопов',
|
||||
many: 'хопов',
|
||||
few: 'хопа',
|
||||
one: 'хоп',
|
||||
);
|
||||
return 'Используется маршрут из $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Введите маршрут вручную';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Текущий маршрут';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Или выберите из контактов:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Недопустимые шестнадцатеричные префиксы: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Установить маршрут';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Управление репитером';
|
||||
|
||||
@@ -2173,16 +2196,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Режим маршрутизации';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Авто (использовать сохранённый маршрут)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Принудительный режим рассылки';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Управление маршрутами';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Обновить';
|
||||
|
||||
@@ -4453,4 +4466,28 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Неизвестно';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-137
@@ -92,6 +92,24 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Zakázať';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Restartovať';
|
||||
|
||||
@@ -782,11 +800,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Počet pokusov o odošleť pred označením správy ako neúspešnej';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batéria';
|
||||
|
||||
@@ -988,6 +1001,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Nová skupina';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Názov skupiny';
|
||||
|
||||
@@ -1469,35 +1491,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Hexová analýza:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Správa ciest';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Režim trasy';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Použiť uloženú cestu';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode =>
|
||||
'Zavrieť režim núdzového povodňového režimu';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Nedávne cesty ACK (klepni na použitie):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'Skok';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'Skákať';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1509,12 +1502,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Úspechy';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Odstrániť cestu';
|
||||
|
||||
@@ -1522,52 +1509,144 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Cesty:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Nastaviť vlastnú cestu';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Ručne zadajte trasu.';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Vyčistiš cestu';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Znovu nájsť vynútene pri nasledujúcej pošlite';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Použite prepínanie trasy v navigačnom paneli.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Celá cesta';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Cesta nastavená: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Uložené lokálne. Spojte sa na synchronizáciu.';
|
||||
@@ -2035,66 +2114,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Zmazať';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Aktívna cesta: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Používa $count $_temp0 cestu';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Zadajte vlastný priebeh';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Aktuálny priebeh';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Cesty (hexové predpony)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Vyberte sa z kontaktov:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Nenašli sa žiadne opakovače ani serverové miestnosti.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášať správky.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Neplatné hexové predpony: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong =>
|
||||
'Cesta je príliš dlhá. Umožnené je maximum 64 skokov.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Nastaviť cestu';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Správa opakérov';
|
||||
|
||||
@@ -2159,16 +2185,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Režim trasy';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Použiť uloženú cestu';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode =>
|
||||
'Zavrieť režim núdzového povodňového režimu';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Správa trás';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Obnoviť';
|
||||
|
||||
@@ -4418,4 +4434,28 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-133
@@ -92,6 +92,24 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Izklopiti';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Ponoviti';
|
||||
|
||||
@@ -783,11 +801,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Baterija';
|
||||
|
||||
@@ -987,6 +1000,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Nova skupina';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Ime skupine';
|
||||
|
||||
@@ -1466,34 +1488,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Izpis heksadecimalnih vrednosti:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Upravljanje poti';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Prikaži vse poti';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Navodilo za usmerjevalni način';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Avto (uporabi shranjeno pot)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Nasilje obvezati v način';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => 'Nedavni poti ACK (tap za uporabo):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'skok';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'skokov';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1505,12 +1499,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Uspešni';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Izbriši pot';
|
||||
|
||||
@@ -1518,51 +1506,144 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Potni ukazi:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Nastavi Prilozeno Pot';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Ročno določite potniško pot.';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Počisti pot';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle => 'Ob naslednji pošiljanju znova zbrati.';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Uporabi tipko usmerjevanja v meniju aplikacije.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Polna pot';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Pot nastavljen: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Shrano lokalno. Povežite se za sinhronizacijo.';
|
||||
@@ -2032,65 +2113,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Ponoviti';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Trenutna pot: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Uporablja $count $_temp0 pot';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Vnesite prilagojeno pot';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Trenutna pot';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Pot (heksafixne skrajšave)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Maksimalno 64 skokov. Vsak prefiks je 2 heksadecimalna znamenja (1 bajt).';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Izberi iz kontaktov:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Ne najdenih ponoviteljev ali strežnikov sob.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Prilojene poti zahtevajo medhodne prenose, ki lahko prenašajo sporočila.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Neveljačni šesteročlenski prefiksi: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => 'Pot je prevelika. Dovoljeno največ 64 skokov.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Nastavi Pot';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Upravljanje ponovitve';
|
||||
|
||||
@@ -2156,15 +2185,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Navodilo za usmerjevalni način';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Avto (uporabi shranjeno pot)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Nasilje obvezati v način';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Upravljanje poti';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Ponovno obnavljati';
|
||||
|
||||
@@ -4416,4 +4436,28 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-133
@@ -92,6 +92,24 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Inaktivera';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Start om';
|
||||
|
||||
@@ -777,11 +795,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batteri';
|
||||
|
||||
@@ -982,6 +995,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Ny grupp';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Gruppnamn';
|
||||
|
||||
@@ -1460,35 +1482,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Hexdump:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Stigarhantering';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Visa alla vägar';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Ruttläge';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Automatisk (använd sparad sökväg)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Tvinga Översvämningsläge';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Nyligen Ack-vägar (tryck för att använda):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Historisk sökväg är full. Ta bort poster för att lägga till nya.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'hoppa';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'hoppar';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1500,12 +1493,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'framgångar';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Ta bort sökväg';
|
||||
|
||||
@@ -1513,50 +1500,144 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Stigar:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Ange anpassad sökväg';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Ange ruttväg manuellt';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Rensa Vägen';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle => 'Tvinga fram omstart vid nästa sändning';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle => 'Använd routningsomkopplaren i appraden';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Fullständig sökväg';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'hoppar',
|
||||
one: 'hopp',
|
||||
);
|
||||
return 'Sökväg inställd: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Sparat lokalt. Anslut för att synkronisera.';
|
||||
@@ -2021,65 +2102,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Rensa';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Nuvarande sökväg: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'hops',
|
||||
one: 'hop',
|
||||
);
|
||||
return 'Använda $count $_temp0 sökväg';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Ange anpassad sökväg';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Nuvarande sökväg';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Ange 2-tecknets hex-prefett för varje hopp, åtskilda med komma.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Hexprefixer';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Välj istället från kontakter:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Inga återuppspelare eller rumsservrar hittades.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Ogiltiga hex-prefikser: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => 'Sökvägen är för lång. Max 64 hopp tillåtna.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Ange Sökväg';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Återuppspelarens Hantering';
|
||||
|
||||
@@ -2144,15 +2173,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Ruttläge';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => 'Automatisk (använd sparad sökväg)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Tvinga Översvämningsläge';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Stigarhantering';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Uppdatera';
|
||||
|
||||
@@ -4391,4 +4411,28 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-140
@@ -92,6 +92,24 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => 'Вимкнути';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => 'Перезавантажити';
|
||||
|
||||
@@ -789,11 +807,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Батарея';
|
||||
|
||||
@@ -995,6 +1008,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => 'Нова група';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => 'Назва групи';
|
||||
|
||||
@@ -1474,35 +1496,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => 'Дамп Hex:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => 'Керування шляхами';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Показати всі шляхи';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Режим маршрутизації';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => 'Авто (використовувати збережений шлях)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => 'Примусово через всю мережу';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths =>
|
||||
'Підтверджені шляхи (натисніть, щоб використати):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull =>
|
||||
'Історія шляхів заповнена. Видаліть записи, щоб додати нові.';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => 'Перехід';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => 'переходів';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
@@ -1516,12 +1509,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => 'Успішно';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Оцінка';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Видалити шлях';
|
||||
|
||||
@@ -1529,54 +1516,144 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get chat_noPathHistoryYet =>
|
||||
'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => 'Дії зі шляхом:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => 'Встановити власний шлях';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => 'Вказати шлях маршрутизації вручну';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => 'Очистити шлях';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Примусово повторити пошук при наступному надсиланні';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
'Шлях очищено. Наступне повідомлення оновить маршрут.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Використовувати перемикач маршрутизації в панелі застосунку';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
'Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => 'Повний шлях';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable =>
|
||||
'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
hopCount,
|
||||
locale: localeName,
|
||||
other: 'переходів',
|
||||
many: 'переходів',
|
||||
few: 'переходи',
|
||||
one: 'перехід',
|
||||
);
|
||||
return 'Шлях встановлено: $hopCount $_temp0 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally =>
|
||||
'Збережено локально. Підключіться для синхронізації.';
|
||||
@@ -2043,67 +2120,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => 'Очистити';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return 'Поточний шлях: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'переходами',
|
||||
many: 'переходами',
|
||||
few: 'переходами',
|
||||
one: 'переходом',
|
||||
);
|
||||
return 'Використання шляху з $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => 'Ввести власний шлях';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => 'Поточний шлях';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions =>
|
||||
'Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample =>
|
||||
'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => 'Hex-префікси';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops =>
|
||||
'Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => 'Вибрати з контактів:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Ретрансляторів або серверів кімнат не знайдено.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return 'Некоректні hex-префікси: $prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => 'Шлях занадто довгий. Максимум 64 переходи.';
|
||||
|
||||
@override
|
||||
String get path_setPath => 'Встановити шлях';
|
||||
|
||||
@override
|
||||
String get repeater_management => 'Керування ретранслятором';
|
||||
|
||||
@@ -2168,16 +2191,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => 'Режим маршрутизації';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath =>
|
||||
'Авто (використовувати збережений шлях)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => 'Примусово через всю мережу';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => 'Керування шляхами';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => 'Оновити';
|
||||
|
||||
@@ -4454,4 +4467,28 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Невідомо';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+177
-113
@@ -92,6 +92,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get common_disable => '禁用';
|
||||
|
||||
@override
|
||||
String get common_undo => 'Undo';
|
||||
|
||||
@override
|
||||
String get messageStatus_sent => 'Sent';
|
||||
|
||||
@override
|
||||
String get messageStatus_delivered => 'Delivered';
|
||||
|
||||
@override
|
||||
String get messageStatus_pending => 'Sending';
|
||||
|
||||
@override
|
||||
String get messageStatus_failed => 'Failed to send';
|
||||
|
||||
@override
|
||||
String get messageStatus_repeated => 'Heard repeated';
|
||||
|
||||
@override
|
||||
String get common_reboot => '重启';
|
||||
|
||||
@@ -735,11 +753,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => '电池';
|
||||
|
||||
@@ -931,6 +944,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_newGroup => '新建群聊';
|
||||
|
||||
@override
|
||||
String get contacts_moreOptions => 'More options';
|
||||
|
||||
@override
|
||||
String get contacts_searchOpen => 'Search contacts';
|
||||
|
||||
@override
|
||||
String get contacts_searchClose => 'Close search';
|
||||
|
||||
@override
|
||||
String get contacts_groupName => '群聊名称';
|
||||
|
||||
@@ -1397,85 +1419,154 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get debugFrame_hexDump => '十六进制数据:';
|
||||
|
||||
@override
|
||||
String get chat_pathManagement => '路径管理';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => '显示所有路径';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => '路由模式';
|
||||
|
||||
@override
|
||||
String get chat_autoUseSavedPath => '自动(使用保存的路径)';
|
||||
|
||||
@override
|
||||
String get chat_forceFloodMode => '强制泛洪模式';
|
||||
|
||||
@override
|
||||
String get chat_recentAckPaths => '最近使用的 ACK 路径(点击使用):';
|
||||
|
||||
@override
|
||||
String get chat_pathHistoryFull => '路径历史已满,请删除后再添加。';
|
||||
|
||||
@override
|
||||
String get chat_hopSingular => '跳';
|
||||
|
||||
@override
|
||||
String get chat_hopPlural => '跳';
|
||||
|
||||
@override
|
||||
String chat_hopsCount(int count) {
|
||||
return '$count 跳';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_successes => '成功';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => '移除路径';
|
||||
|
||||
@override
|
||||
String get chat_noPathHistoryYet => '暂无路径历史。\n发送消息以探索路径。';
|
||||
|
||||
@override
|
||||
String get chat_pathActions => '路径操作:';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPath => '设置自定义路径';
|
||||
|
||||
@override
|
||||
String get chat_setCustomPathSubtitle => '手动指定路由路径';
|
||||
|
||||
@override
|
||||
String get chat_clearPath => '清除路径';
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle => '清除当前路径,下次发送将重新尝试。';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared => '路径已清除。下一条消息将重新路由。';
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle => '在应用栏中切换路由模式。';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled => '泛洪模式已启用。可通过应用栏的路由图标切换。';
|
||||
|
||||
@override
|
||||
String get chat_fullPath => '完整路径';
|
||||
|
||||
@override
|
||||
String get chat_pathDetailsNotAvailable => '路径信息暂不可用,请尝试发送消息刷新。';
|
||||
String get routing_title => 'Routing';
|
||||
|
||||
@override
|
||||
String chat_pathSetHops(int hopCount, String status) {
|
||||
return '路径设置:$hopCount 跳 - $status';
|
||||
String get routing_modeAuto => 'Auto';
|
||||
|
||||
@override
|
||||
String get routing_modeFlood => 'Flood';
|
||||
|
||||
@override
|
||||
String get routing_modeManual => 'Manual';
|
||||
|
||||
@override
|
||||
String get routing_modeAutoHint =>
|
||||
'Picks the best known path automatically, flooding when none is known.';
|
||||
|
||||
@override
|
||||
String get routing_modeFloodHint =>
|
||||
'Broadcasts through every repeater. Most reliable, but uses more airtime.';
|
||||
|
||||
@override
|
||||
String get routing_modeManualHint =>
|
||||
'Always sends along the exact path you set.';
|
||||
|
||||
@override
|
||||
String get routing_currentRoute => 'Current route';
|
||||
|
||||
@override
|
||||
String get routing_directNoHops => 'Direct — no repeater hops';
|
||||
|
||||
@override
|
||||
String get routing_noPathYet =>
|
||||
'No path yet. The next message floods until a route is discovered.';
|
||||
|
||||
@override
|
||||
String get routing_floodBroadcast => 'Broadcast through every repeater';
|
||||
|
||||
@override
|
||||
String get routing_editPath => 'Edit path';
|
||||
|
||||
@override
|
||||
String get routing_forgetPath => 'Forget path';
|
||||
|
||||
@override
|
||||
String get routing_knownPaths => 'Known paths';
|
||||
|
||||
@override
|
||||
String get routing_knownPathsHint => 'Tap a path to switch to it.';
|
||||
|
||||
@override
|
||||
String get routing_inUse => 'In use';
|
||||
|
||||
@override
|
||||
String get routing_qualityStrong => 'Strong first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityGood => 'Good first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityFair => 'Fair first hop';
|
||||
|
||||
@override
|
||||
String get routing_qualityWorked => 'Has delivered';
|
||||
|
||||
@override
|
||||
String get routing_qualityFlood => 'Heard via flood';
|
||||
|
||||
@override
|
||||
String get routing_qualityUntested => 'Untested';
|
||||
|
||||
@override
|
||||
String routing_lastWorked(String when) {
|
||||
return 'worked $when';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_neverWorked => 'never confirmed';
|
||||
|
||||
@override
|
||||
String routing_deliveryCounts(int successes, int failures) {
|
||||
return '$successes delivered, $failures failed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get routing_floodDelivery => 'Flood delivery';
|
||||
|
||||
@override
|
||||
String get pathEditor_title => 'Build Path';
|
||||
|
||||
@override
|
||||
String pathEditor_hopCounter(int count) {
|
||||
return '$count of 64 hops';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_noHops =>
|
||||
'No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.';
|
||||
|
||||
@override
|
||||
String get pathEditor_addHops => 'Add hops in order';
|
||||
|
||||
@override
|
||||
String get pathEditor_searchRepeaters => 'Search repeaters';
|
||||
|
||||
@override
|
||||
String get pathEditor_advancedHex => 'Advanced: raw hex path';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexLabel => 'Hex prefixes';
|
||||
|
||||
@override
|
||||
String get pathEditor_hexHelper =>
|
||||
'Two hex characters per hop, separated by commas';
|
||||
|
||||
@override
|
||||
String pathEditor_invalidTokens(String tokens) {
|
||||
return 'Invalid: $tokens';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathEditor_tooManyHops => 'Maximum 64 hops';
|
||||
|
||||
@override
|
||||
String get pathEditor_usePath => 'Use this path';
|
||||
|
||||
@override
|
||||
String get pathEditor_removeHop => 'Remove hop';
|
||||
|
||||
@override
|
||||
String get pathEditor_unknownHop => 'Unknown repeater';
|
||||
|
||||
@override
|
||||
String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。';
|
||||
|
||||
@@ -1928,54 +2019,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get common_clear => '清除';
|
||||
|
||||
@override
|
||||
String path_currentPath(String path) {
|
||||
return '当前路径:$path';
|
||||
}
|
||||
|
||||
@override
|
||||
String path_usingHopsPath(int count) {
|
||||
return '使用 $count 跳路径';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_enterCustomPath => '输入自定义路径';
|
||||
|
||||
@override
|
||||
String get path_currentPathLabel => '当前路径';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixInstructions => '请输入每个中继节点的2字符十六进制前缀,用逗号分隔。';
|
||||
|
||||
@override
|
||||
String get path_hexPrefixExample => '例如:A1, F2, 3C(每个节点使用其公钥的第一字节)';
|
||||
|
||||
@override
|
||||
String get path_labelHexPrefixes => '路径(十六进制前缀)';
|
||||
|
||||
@override
|
||||
String get path_helperMaxHops => '最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。';
|
||||
|
||||
@override
|
||||
String get path_selectFromContacts => '或从联系人列表中选择:';
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound => '未找到任何转发节点或房间服务器。';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire => '自定义路径需要中间节点转发消息。';
|
||||
|
||||
@override
|
||||
String path_invalidHexPrefixes(String prefixes) {
|
||||
return '无效的十六进制前缀:$prefixes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path_tooLong => '路径过长,最多允许 64 跳。';
|
||||
|
||||
@override
|
||||
String get path_setPath => '设置路径';
|
||||
|
||||
@override
|
||||
String get repeater_management => '转发节点管理';
|
||||
|
||||
@@ -2036,15 +2085,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_routingMode => '路由模式';
|
||||
|
||||
@override
|
||||
String get repeater_autoUseSavedPath => '自动(使用保存的路径)';
|
||||
|
||||
@override
|
||||
String get repeater_forceFloodMode => '强制泛洪模式';
|
||||
|
||||
@override
|
||||
String get repeater_pathManagement => '路径管理';
|
||||
|
||||
@override
|
||||
String get repeater_refresh => '刷新';
|
||||
|
||||
@@ -4086,4 +4126,28 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get map_zoomIn => 'Zoom in';
|
||||
|
||||
@override
|
||||
String get map_zoomOut => 'Zoom out';
|
||||
|
||||
@override
|
||||
String get map_centerMap => 'Center map';
|
||||
|
||||
@override
|
||||
String get chrome_bluetoothRequiresChromium =>
|
||||
'Web Bluetooth requires a Chromium browser';
|
||||
|
||||
@override
|
||||
String channels_communityShortId(String id) {
|
||||
return 'ID: $id...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathTrace_legendGpsConfirmed => 'GPS confirmed';
|
||||
|
||||
@override
|
||||
String get pathTrace_legendInferred => 'Inferred position';
|
||||
}
|
||||
|
||||
+12
-17
@@ -24,11 +24,21 @@ import 'services/translation_service.dart';
|
||||
import 'services/ui_view_state_service.dart';
|
||||
import 'services/timeout_prediction_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
import 'theme/mesh_theme.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// On desktop, debugPrint is not suppressed in release builds and every
|
||||
// call is a synchronous stdout write. The connector logs heavily on hot
|
||||
// paths (frame handling, queue/channel sync), which shows up as syscall
|
||||
// overhead on low-end Linux machines (issue #202). The in-app debug log
|
||||
// screens are unaffected — they store entries themselves.
|
||||
if (kReleaseMode) {
|
||||
debugPrint = (String? message, {int? wrapWidth}) {};
|
||||
}
|
||||
|
||||
// Initialize SharedPreferences cache
|
||||
await PrefsManager.initialize();
|
||||
|
||||
@@ -191,23 +201,8 @@ class MeshCoreApp extends StatelessWidget {
|
||||
locale: _localeFromSetting(
|
||||
settingsService.settings.languageOverride,
|
||||
),
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
),
|
||||
theme: MeshTheme.light(),
|
||||
darkTheme: MeshTheme.dark(),
|
||||
themeMode: _themeModeFromSetting(
|
||||
settingsService.settings.themeMode,
|
||||
),
|
||||
|
||||
@@ -63,7 +63,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
final entry = entries[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: _buildLevelIcon(entry.level),
|
||||
leading: _buildLevelIcon(context, entry.level),
|
||||
title: Text(
|
||||
'[${entry.tag}] ${entry.message}',
|
||||
style: const TextStyle(
|
||||
@@ -75,7 +75,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
entry.formattedTime,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -88,14 +88,14 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
Icon(
|
||||
Icons.bug_report_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.debugLog_noEntries,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -103,7 +103,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
context.l10n.debugLog_enableInSettings,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -115,18 +115,19 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevelIcon(AppDebugLogLevel level) {
|
||||
Widget _buildLevelIcon(BuildContext context, AppDebugLogLevel level) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
switch (level) {
|
||||
case AppDebugLogLevel.info:
|
||||
return const Icon(Icons.info_outline, size: 18, color: Colors.blue);
|
||||
return Icon(Icons.info_outline, size: 18, color: colorScheme.primary);
|
||||
case AppDebugLogLevel.warning:
|
||||
return const Icon(
|
||||
return Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
size: 18,
|
||||
color: Colors.orange,
|
||||
color: colorScheme.tertiary,
|
||||
);
|
||||
case AppDebugLogLevel.error:
|
||||
return const Icon(Icons.error_outline, size: 18, color: Colors.red);
|
||||
return Icon(Icons.error_outline, size: 18, color: colorScheme.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,29 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.brightness_6_outlined),
|
||||
title: Text(context.l10n.appSettings_theme),
|
||||
subtitle: Text(
|
||||
_themeModeLabel(context, settingsService.settings.themeMode),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: SegmentedButton<String>(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: 'system',
|
||||
label: Text(context.l10n.appSettings_themeSystem),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: 'light',
|
||||
label: Text(context.l10n.appSettings_themeLight),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: 'dark',
|
||||
label: Text(context.l10n.appSettings_themeDark),
|
||||
),
|
||||
],
|
||||
selected: {settingsService.settings.themeMode},
|
||||
onSelectionChanged: (selection) {
|
||||
settingsService.setThemeMode(selection.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showThemeModeDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
@@ -111,18 +129,6 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showLanguageDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.location_searching),
|
||||
title: Text(context.l10n.appSettings_enableMessageTracing),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_enableMessageTracingSubtitle,
|
||||
),
|
||||
value: settingsService.settings.enableMessageTracing,
|
||||
onChanged: (value) {
|
||||
settingsService.setEnableMessageTracing(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -189,14 +195,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
Icons.message_outlined,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.appSettings_messageNotifications,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
@@ -204,7 +210,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
value: settingsService.settings.notifyOnNewMessage,
|
||||
@@ -220,14 +226,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
Icons.forum_outlined,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.appSettings_channelMessageNotifications,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
@@ -235,7 +241,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
value: settingsService.settings.notifyOnNewChannelMessage,
|
||||
@@ -251,14 +257,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
Icons.cell_tower,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.appSettings_advertisementNotifications,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
@@ -266,7 +272,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
value: settingsService.settings.notifyOnNewAdvert,
|
||||
@@ -343,10 +349,16 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (settingsService.settings.autoRouteRotationEnabled) ...[
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_maxRouteWeight),
|
||||
if (settingsService.settings.autoRouteRotationEnabled)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_maxRouteWeight),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -454,7 +466,21 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.location_searching),
|
||||
title: Text(context.l10n.appSettings_enableMessageTracing),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_enableMessageTracingSubtitle,
|
||||
),
|
||||
value: settingsService.settings.enableMessageTracing,
|
||||
onChanged: (value) {
|
||||
settingsService.setEnableMessageTracing(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -584,15 +610,15 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
SwitchListTile(
|
||||
secondary: Icon(
|
||||
Icons.auto_awesome_outlined,
|
||||
color: translationEnabled ? null : Colors.grey,
|
||||
color: translationEnabled ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.translation_autoIncomingTitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
|
||||
),
|
||||
subtitle: Text(
|
||||
context.l10n.translation_autoIncomingSubtitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
|
||||
),
|
||||
value: settings.autoTranslateIncomingMessages,
|
||||
onChanged: translationEnabled
|
||||
@@ -603,15 +629,15 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
SwitchListTile(
|
||||
secondary: Icon(
|
||||
Icons.outgoing_mail,
|
||||
color: translationEnabled ? null : Colors.grey,
|
||||
color: translationEnabled ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.translation_composerTitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
|
||||
),
|
||||
subtitle: Text(
|
||||
context.l10n.translation_composerSubtitle,
|
||||
style: TextStyle(color: translationEnabled ? null : Colors.grey),
|
||||
style: TextStyle(color: translationEnabled ? null : Theme.of(context).disabledColor),
|
||||
),
|
||||
value: settings.composerTranslationEnabled,
|
||||
onChanged: translationEnabled
|
||||
@@ -871,61 +897,6 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showThemeModeDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_theme),
|
||||
content: RadioGroup<String>(
|
||||
groupValue: settingsService.settings.themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsService.setThemeMode(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
title: Text(context.l10n.appSettings_themeSystem),
|
||||
value: 'system',
|
||||
),
|
||||
RadioListTile<String>(
|
||||
title: Text(context.l10n.appSettings_themeLight),
|
||||
value: 'light',
|
||||
),
|
||||
RadioListTile<String>(
|
||||
title: Text(context.l10n.appSettings_themeDark),
|
||||
value: 'dark',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _themeModeLabel(BuildContext context, String value) {
|
||||
switch (value) {
|
||||
case 'light':
|
||||
return context.l10n.appSettings_themeLight;
|
||||
case 'dark':
|
||||
return context.l10n.appSettings_themeDark;
|
||||
default:
|
||||
return context.l10n.appSettings_themeSystem;
|
||||
}
|
||||
}
|
||||
|
||||
String _languageLabel(BuildContext context, String? languageCode) {
|
||||
switch (languageCode) {
|
||||
case 'en':
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
@@ -25,8 +25,9 @@ import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../helpers/contact_ui.dart';
|
||||
import '../widgets/byte_count_input.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
@@ -283,6 +284,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
)
|
||||
: widget.channel.name,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
@@ -311,9 +314,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
if (value == 'clearChat') {
|
||||
context.read<MeshCoreConnector>().clearMessagesForChannel(
|
||||
widget.channel.index,
|
||||
);
|
||||
_confirmClearChat();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
@@ -321,11 +322,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
value: 'clearChat',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
Icon(
|
||||
Icons.delete,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.contact_clearChat,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -344,34 +351,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
|
||||
if (messages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
widget.channel.isPublicChannel
|
||||
? Icons.public
|
||||
: Icons.tag,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.chat_noMessages,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.chat_sendMessageToStart,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
return EmptyState(
|
||||
icon: widget.channel.isPublicChannel
|
||||
? Icons.public
|
||||
: Icons.tag,
|
||||
title: context.l10n.chat_noMessages,
|
||||
subtitle: context.l10n.chat_sendMessageTo(
|
||||
widget.channel.name.isEmpty
|
||||
? context.l10n.channels_channelIndex(
|
||||
widget.channel.index,
|
||||
)
|
||||
: widget.channel.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -381,6 +371,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final itemCount =
|
||||
reversedMessages.length + (_isLoadingOlder ? 1 : 0);
|
||||
|
||||
// Prune stale keys (deleted/cleared messages) to avoid
|
||||
// unbounded growth.
|
||||
final liveIds = reversedMessages
|
||||
.map((m) => m.messageId)
|
||||
.toSet();
|
||||
_messageKeys.removeWhere((id, _) => !liveIds.contains(id));
|
||||
|
||||
// Two messages can collide on messageId (same ms + name/text
|
||||
// hash). Only the first occurrence owns the shared GlobalKey
|
||||
// used for scroll-to-message; duplicates get a local key so
|
||||
// no two widgets share one GlobalKey.
|
||||
final seenIds = <String>{};
|
||||
final keyedIndices = <int>{};
|
||||
for (var i = 0; i < reversedMessages.length; i++) {
|
||||
if (seenIds.add(reversedMessages[i].messageId)) {
|
||||
keyedIndices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom if user is already at bottom
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_channelSkipNextBottomSnap) {
|
||||
@@ -416,14 +425,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
final messageIndex = index;
|
||||
final message = reversedMessages[messageIndex];
|
||||
if (!_messageKeys.containsKey(message.messageId)) {
|
||||
_messageKeys[message.messageId] = GlobalKey();
|
||||
final GlobalKey messageKey;
|
||||
if (keyedIndices.contains(messageIndex)) {
|
||||
messageKey = _messageKeys.putIfAbsent(
|
||||
message.messageId,
|
||||
GlobalKey.new,
|
||||
);
|
||||
} else {
|
||||
messageKey = GlobalKey();
|
||||
}
|
||||
final isUnreadAnchor =
|
||||
_unreadDividerMessageId != null &&
|
||||
message.messageId == _unreadDividerMessageId;
|
||||
return Container(
|
||||
key: _messageKeys[message.messageId]!,
|
||||
key: messageKey,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final textScale = context
|
||||
@@ -495,7 +510,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
const maxSwipeOffset = 64.0;
|
||||
const replySwipeThreshold = 64.0;
|
||||
const bodyFontSize = 14.0;
|
||||
final messageBody = Column(
|
||||
final messageBody = LayoutBuilder(
|
||||
builder: (context, constraints) => Column(
|
||||
crossAxisAlignment: isOutgoing
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
@@ -512,9 +528,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
],
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: PlatformInfo.isDesktop
|
||||
? null
|
||||
: () => _showMessagePathInfo(message),
|
||||
onLongPress: () => _showMessageActions(message),
|
||||
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||
? (_) => _showMessageActions(message)
|
||||
@@ -524,7 +537,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
? const EdgeInsets.all(4)
|
||||
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
||||
maxWidth: constraints.maxWidth * 0.65,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutgoing
|
||||
@@ -566,20 +579,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
isOutgoing,
|
||||
textScale,
|
||||
message.senderName,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
else if (gifId != null)
|
||||
Stack(
|
||||
@@ -599,36 +598,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutgoing
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
@@ -651,97 +620,89 @@ 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 && displayPath.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(displayPath),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(context, message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (enableTracing && message.repeatCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
Icons.repeat,
|
||||
size: 12 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'${message.repeatCount}',
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent,
|
||||
isRepeated:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isPending:
|
||||
message.status ==
|
||||
ChannelMessageStatus.pending,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (enableTracing) ...[
|
||||
if (displayPath.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(displayPath),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(context, message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
if (message.repeatCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
Icons.repeat,
|
||||
size: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'${message.repeatCount}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
message.status == ChannelMessageStatus.sent
|
||||
? Icons.check
|
||||
: message.status ==
|
||||
ChannelMessageStatus.pending
|
||||
? Icons.schedule
|
||||
: Icons.error_outline,
|
||||
size: 14,
|
||||
color:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed
|
||||
? Colors.red
|
||||
: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -757,6 +718,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!isOutgoing && !PlatformInfo.isDesktop) {
|
||||
@@ -958,9 +920,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
IconButton(
|
||||
icon: Icon(Icons.location_on_outlined, color: channelColor),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||
onPressed: () {
|
||||
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
|
||||
final selfName = context.read<MeshCoreConnector>().selfName ?? context.l10n.chat_me;
|
||||
final fromName = isOutgoing ? selfName : senderName;
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: 'channel:${widget.channel.index}',
|
||||
@@ -1020,8 +982,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
|
||||
Widget _buildAvatar(String senderName) {
|
||||
final initial = _getFirstCharacterOrEmoji(senderName);
|
||||
final color = _getColorForName(senderName);
|
||||
final initial = firstCharacterOrEmoji(senderName);
|
||||
final color = colorForName(senderName);
|
||||
|
||||
return CircleAvatar(
|
||||
radius: 18,
|
||||
@@ -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) {
|
||||
final message = _replyingToMessage!;
|
||||
return Container(
|
||||
@@ -1116,8 +1048,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: _cancelReply,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1412,15 +1343,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_setReplyingTo(message);
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.isDesktop)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.route),
|
||||
title: Text(context.l10n.chat_path),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showMessagePathInfo(message);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.route),
|
||||
title: Text(context.l10n.chat_path),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showMessagePathInfo(message);
|
||||
},
|
||||
),
|
||||
// Can't react to your own messages
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
@@ -1463,19 +1393,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_markAsUnread(message);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: Text(context.l10n.common_delete),
|
||||
leading: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.common_delete,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await _deleteMessage(message);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.close),
|
||||
title: Text(context.l10n.common_cancel),
|
||||
onTap: () => Navigator.pop(sheetContext),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -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 {
|
||||
await context.read<MeshCoreConnector>().deleteChannelMessage(message);
|
||||
if (!mounted) return;
|
||||
@@ -1557,6 +1517,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
double _maxSwipeDistance = 0;
|
||||
int? _swipePointerId;
|
||||
bool _swipeLockedToHorizontal = false;
|
||||
bool _isRtl = false;
|
||||
|
||||
void _handleSwipeStart(Offset position) {
|
||||
_swipeStartPosition = position;
|
||||
@@ -1577,11 +1538,13 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
return;
|
||||
}
|
||||
|
||||
final dx = event.position.dx - _swipeStartPosition!.dx;
|
||||
final rawDx = event.position.dx - _swipeStartPosition!.dx;
|
||||
// In LTR swipe left (rawDx < 0) triggers reply; in RTL swipe right (rawDx > 0).
|
||||
final signedDx = _isRtl ? rawDx : -rawDx;
|
||||
|
||||
const axisLockThreshold = 12.0;
|
||||
if (!_swipeLockedToHorizontal) {
|
||||
if (-dx < axisLockThreshold) {
|
||||
if (signedDx < axisLockThreshold) {
|
||||
return;
|
||||
}
|
||||
_swipeLockedToHorizontal = true;
|
||||
@@ -1593,28 +1556,32 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
void _handleSwipeUpdate(Offset position) {
|
||||
if (_swipeStartPosition == null) return;
|
||||
|
||||
final dx = position.dx - _swipeStartPosition!.dx;
|
||||
if (dx >= 0) return;
|
||||
final rawDx = position.dx - _swipeStartPosition!.dx;
|
||||
final signedDx = _isRtl ? rawDx : -rawDx;
|
||||
if (signedDx <= 0) return;
|
||||
|
||||
if (-dx < 6) return;
|
||||
if (signedDx < 6) return;
|
||||
|
||||
if (-dx > _maxSwipeDistance) {
|
||||
_maxSwipeDistance = -dx;
|
||||
if (signedDx > _maxSwipeDistance) {
|
||||
_maxSwipeDistance = signedDx;
|
||||
}
|
||||
|
||||
final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble();
|
||||
final double clamped = signedDx.clamp(0.0, widget.maxSwipeOffset);
|
||||
final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
|
||||
if (adjusted != _swipeOffset) {
|
||||
setState(() => _swipeOffset = adjusted);
|
||||
// Translate in the gesture direction: negative for LTR (left), positive for RTL (right).
|
||||
final translationOffset = _isRtl ? adjusted : -adjusted;
|
||||
if (translationOffset != _swipeOffset) {
|
||||
setState(() => _swipeOffset = translationOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSwipePointerUp(Offset position) {
|
||||
if (_swipeLockedToHorizontal && _swipeStartPosition != null) {
|
||||
final dx = position.dx - _swipeStartPosition!.dx;
|
||||
final rawDx = position.dx - _swipeStartPosition!.dx;
|
||||
final signedDx = _isRtl ? rawDx : -rawDx;
|
||||
final peak = math.max(
|
||||
_maxSwipeDistance,
|
||||
(-dx).clamp(0.0, double.infinity),
|
||||
signedDx.clamp(0.0, double.infinity),
|
||||
);
|
||||
if (peak >= widget.replySwipeThreshold) {
|
||||
widget.onReplyTriggered();
|
||||
@@ -1654,6 +1621,10 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_isRtl = Directionality.of(context) == TextDirection.rtl;
|
||||
// In LTR, the bubble slides left and the hint appears on the right (isStart: false).
|
||||
// In RTL, the bubble slides right and the hint appears on the left (isStart: true).
|
||||
final hintIsStart = _isRtl;
|
||||
return Listener(
|
||||
onPointerDown: _handleSwipePointerDown,
|
||||
onPointerMove: _handleSwipePointerMove,
|
||||
@@ -1667,7 +1638,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
Positioned.fill(
|
||||
child: Opacity(
|
||||
opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
|
||||
child: widget.hintBuilder(isStart: false),
|
||||
child: widget.hintBuilder(isStart: hintIsStart),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
|
||||
@@ -106,7 +106,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
if (!hasHopDetails)
|
||||
Text(
|
||||
l10n.channelPath_noHopDetails,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
else
|
||||
..._buildHopTiles(context, hops),
|
||||
@@ -131,22 +131,25 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildDetailRow(l10n.channelPath_senderLabel, message.senderName),
|
||||
_buildDetailRow(context, l10n.channelPath_senderLabel, message.senderName),
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.channelPath_timeLabel,
|
||||
_formatTime(message.timestamp, l10n),
|
||||
),
|
||||
if (message.repeatCount > 0)
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.channelPath_repeatsLabel,
|
||||
message.repeatCount.toString(),
|
||||
),
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.channelPath_pathLabelTitle,
|
||||
_formatPathLabel(message.pathLength, l10n),
|
||||
),
|
||||
if (observedLabel != null)
|
||||
_buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
|
||||
_buildDetailRow(context, l10n.channelPath_observedLabel, observedLabel),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -250,7 +253,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
return l10n.channelPath_observedSomeOf(observedCount, pathLength);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
Widget _buildDetailRow(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
@@ -258,7 +261,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||
child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
@@ -412,17 +415,17 @@ class _ChannelMessagePathMapScreenState
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
@@ -593,7 +596,7 @@ class _ChannelMessagePathMapScreenState
|
||||
if (points.isEmpty)
|
||||
Center(
|
||||
child: Card(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Text(
|
||||
@@ -664,7 +667,7 @@ class _ChannelMessagePathMapScreenState
|
||||
label,
|
||||
_formatPathPrefixes(selectedPath.pathBytes),
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[700], fontSize: 12),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -685,28 +688,32 @@ class _ChannelMessagePathMapScreenState
|
||||
markers.add(
|
||||
Marker(
|
||||
point: point,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hop.index.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hop.index.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -729,28 +736,32 @@ class _ChannelMessagePathMapScreenState
|
||||
markers.add(
|
||||
Marker(
|
||||
point: selfPoint,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.l10n.pathTrace_you,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.l10n.pathTrace_you,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -804,6 +815,12 @@ class _ChannelMessagePathMapScreenState
|
||||
);
|
||||
}
|
||||
|
||||
Widget _colorDot(Color color) => Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
);
|
||||
|
||||
Widget _buildLegendCard(
|
||||
BuildContext context,
|
||||
List<_PathHop> hops,
|
||||
@@ -826,9 +843,22 @@ class _ChannelMessagePathMapScreenState
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
_colorDot(Colors.green),
|
||||
const SizedBox(width: 4),
|
||||
Text(l10n.pathTrace_legendGpsConfirmed, style: const TextStyle(fontSize: 11)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
@@ -111,24 +111,26 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout, color: Colors.red),
|
||||
Icon(
|
||||
Icons.logout,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.common_disconnect),
|
||||
],
|
||||
),
|
||||
onTap: () => _disconnect(context),
|
||||
),
|
||||
if (_communities.isNotEmpty)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.groups),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.community_manageCommunities),
|
||||
],
|
||||
),
|
||||
onTap: () => _showManageCommunitiesDialog(context),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.groups),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.community_manageCommunities),
|
||||
],
|
||||
),
|
||||
onTap: () => _showManageCommunitiesDialog(context),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -241,32 +243,22 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
),
|
||||
Expanded(
|
||||
child: filteredChannels.isEmpty
|
||||
? ListView(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height - 300,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
? LayoutBuilder(
|
||||
builder: (context, constraints) => ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: EmptyState(
|
||||
icon: Icons.search_off,
|
||||
title:
|
||||
context.l10n.channels_noChannelsFound,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
: (viewState.channelsSortOption ==
|
||||
ChannelSortOption.manual &&
|
||||
@@ -357,6 +349,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
int? dragIndex,
|
||||
}) {
|
||||
final unreadCount = connector.getUnreadCountForChannel(channel);
|
||||
final isMuted = context.watch<AppSettingsService>().isChannelMuted(
|
||||
channel.name,
|
||||
);
|
||||
|
||||
// Determine icon and colors based on channel type
|
||||
IconData icon;
|
||||
@@ -449,37 +444,45 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isMuted) ...[
|
||||
Icon(
|
||||
Icons.notifications_off,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (unreadCount > 0) ...[
|
||||
UnreadBadge(count: unreadCount),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (showDragHandle && dragIndex != null)
|
||||
ReorderableDelayedDragStartListener(
|
||||
ReorderableDragStartListener(
|
||||
index: dragIndex,
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
onTap: () {
|
||||
final unread = connector.getUnreadCountForChannelIndex(
|
||||
channel.index,
|
||||
);
|
||||
connector.markChannelRead(channel.index);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelChatScreen(
|
||||
channel: channel,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelChatScreen(
|
||||
channel: channel,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
},
|
||||
onLongPress: () => _showChannelActions(
|
||||
context,
|
||||
@@ -540,10 +543,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
leading: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Theme.of(sheetContext).colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.channels_deleteChannel,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(
|
||||
color: Theme.of(sheetContext).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
@@ -725,23 +733,39 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
? (isSelected
|
||||
? Theme.of(dialogContext).colorScheme.primaryContainer
|
||||
: null)
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
: Theme.of(
|
||||
dialogContext,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.12),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: enabled
|
||||
? (isSelected
|
||||
? Theme.of(dialogContext).colorScheme.primary
|
||||
: null)
|
||||
: Colors.grey,
|
||||
: Theme.of(
|
||||
dialogContext,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.38),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(color: enabled ? null : Colors.grey),
|
||||
style: TextStyle(
|
||||
color: enabled
|
||||
? null
|
||||
: Theme.of(
|
||||
dialogContext,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.38),
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: enabled ? null : Colors.grey),
|
||||
style: TextStyle(
|
||||
color: enabled
|
||||
? null
|
||||
: Theme.of(
|
||||
dialogContext,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.38),
|
||||
),
|
||||
),
|
||||
trailing: enabled ? const Icon(Icons.chevron_right) : null,
|
||||
selected: isSelected,
|
||||
@@ -929,7 +953,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Channel.publicChannelPsk,
|
||||
);
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, 'Public', psk);
|
||||
connector.setChannel(nextIndex, context.l10n.channels_public, psk);
|
||||
if (context.mounted) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
@@ -1043,7 +1067,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
dialogContext.l10n.community_hashtagPrivacyHint,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(
|
||||
dialogContext,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
@@ -1214,6 +1240,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
final publicLabel = context.l10n.channels_public;
|
||||
if (name.isEmpty) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
@@ -1238,7 +1265,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
final psk = community
|
||||
.deriveCommunityPublicPsk();
|
||||
final channelName =
|
||||
'${community.name} Public';
|
||||
'${community.name} $publicLabel';
|
||||
connector.setChannel(
|
||||
nextIndex,
|
||||
channelName,
|
||||
@@ -1594,7 +1621,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
},
|
||||
child: Text(
|
||||
dialogContext.l10n.common_delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1604,7 +1631,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
|
||||
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
|
||||
final psk = Channel.parsePskHex(Channel.publicChannelPsk);
|
||||
connector.setChannel(0, 'Public', psk);
|
||||
connector.setChannel(0, context.l10n.channels_public, psk);
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.channels_publicChannelAdded),
|
||||
@@ -1653,14 +1680,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Icon(
|
||||
Icons.groups_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.community_noCommunities,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -1668,7 +1700,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
context.l10n.community_scanOrCreate,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[500],
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -1692,10 +1727,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
),
|
||||
title: Text(community.name),
|
||||
subtitle: Text(
|
||||
'ID: ${community.shortCommunityId}...',
|
||||
context.l10n.channels_communityShortId(community.shortCommunityId),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
@@ -1722,14 +1759,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
value: 'leave',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.exit_to_app,
|
||||
color: Colors.red,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.community_delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1830,7 +1873,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
},
|
||||
child: Text(
|
||||
dialogContext.l10n.community_delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+198
-833
File diff suppressed because it is too large
Load Diff
@@ -8,34 +8,25 @@ class ChromeRequiredScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
final colorScheme = theme.colorScheme;
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isDark
|
||||
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
|
||||
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
|
||||
),
|
||||
),
|
||||
color: colorScheme.surface,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
color: colorScheme.tertiaryContainer.withValues(alpha: 0.4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.browser_not_supported_rounded,
|
||||
size: 80,
|
||||
color: Colors.orange,
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -44,7 +35,7 @@ class ChromeRequiredScreen extends StatelessWidget {
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -52,7 +43,7 @@ class ChromeRequiredScreen extends StatelessWidget {
|
||||
l10n.scanner_chromeRequiredMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -62,19 +53,19 @@ class ChromeRequiredScreen extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.1),
|
||||
color: colorScheme.secondaryContainer.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||
border: Border.all(color: colorScheme.outline.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
|
||||
Icon(Icons.info_outline, size: 20, color: colorScheme.secondary),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"Web Bluetooth requires a Chromium browser",
|
||||
l10n.chrome_bluetoothRequiresChromium,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.blue,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -210,10 +210,10 @@ class _NoiseChartPainter extends CustomPainter {
|
||||
}
|
||||
final span = maxV - minV;
|
||||
|
||||
for (var i = 0; i <= 2; i++) {
|
||||
final v = maxV - span * i / 2;
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
final v = maxV - span * i / 4;
|
||||
final tp = _yAxisLabel(v);
|
||||
final y = chart.top + (chart.height * i / 2) - tp.height / 2;
|
||||
final y = chart.top + (chart.height * i / 4) - tp.height / 2;
|
||||
tp.paint(canvas, Offset(4, y));
|
||||
}
|
||||
|
||||
|
||||
+235
-179
@@ -29,6 +29,7 @@ import '../widgets/repeater_login_dialog.dart';
|
||||
import '../widgets/room_login_dialog.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/contact_ui.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
@@ -59,7 +60,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
String _loadedGroupScopeKeyHex = '';
|
||||
Timer? _searchDebounce;
|
||||
|
||||
final Set<ContactOperationType> _pendingOperations = {};
|
||||
final List<ContactOperationType> _pendingOperations = [];
|
||||
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
|
||||
@@ -185,59 +186,52 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||
}
|
||||
|
||||
// Generic OK/ERR acks carry no command correlation, so consume only
|
||||
// the oldest pending operation per ack instead of clearing all.
|
||||
if (code == respCodeOk) {
|
||||
// Show a snackbar indicating success
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactImported),
|
||||
);
|
||||
if (_pendingOperations.isEmpty) return;
|
||||
final op = _pendingOperations.removeAt(0);
|
||||
switch (op) {
|
||||
case ContactOperationType.import:
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactImported),
|
||||
);
|
||||
case ContactOperationType.zeroHopShare:
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||
);
|
||||
case ContactOperationType.export:
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
);
|
||||
}
|
||||
|
||||
_pendingOperations.clear();
|
||||
}
|
||||
|
||||
if (code == respCodeErr) {
|
||||
// Show a snackbar indicating failure
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactImportFailed),
|
||||
);
|
||||
if (_pendingOperations.isEmpty) return;
|
||||
final op = _pendingOperations.removeAt(0);
|
||||
switch (op) {
|
||||
case ContactOperationType.import:
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactImportFailed),
|
||||
);
|
||||
case ContactOperationType.zeroHopShare:
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
);
|
||||
case ContactOperationType.export:
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
);
|
||||
}
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
);
|
||||
}
|
||||
|
||||
_pendingOperations.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.error(
|
||||
@@ -252,17 +246,37 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final exportContactFrame = buildExportContactFrame(pubKey);
|
||||
_pendingOperations.add(ContactOperationType.export);
|
||||
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
|
||||
try {
|
||||
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
|
||||
} catch (e) {
|
||||
_pendingOperations.remove(ContactOperationType.export);
|
||||
if (mounted) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _contactZeroHop(Uint8List pubKey) async {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
|
||||
_pendingOperations.add(ContactOperationType.zeroHopShare);
|
||||
await connector.sendFrame(
|
||||
exportContactZeroHopFrame,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
try {
|
||||
await connector.sendFrame(
|
||||
exportContactZeroHopFrame,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
} catch (e) {
|
||||
_pendingOperations.remove(ContactOperationType.zeroHopShare);
|
||||
if (mounted) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _contactImport() async {
|
||||
@@ -288,11 +302,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return;
|
||||
}
|
||||
final hexString = text.substring('meshcore://'.length);
|
||||
final Uint8List importContactFrame;
|
||||
try {
|
||||
final bytes = hex2Uint8List(hexString);
|
||||
final importContactFrame = buildImportContactFrame(bytes);
|
||||
_pendingOperations.add(ContactOperationType.import);
|
||||
connector.importContact(importContactFrame);
|
||||
importContactFrame = buildImportContactFrame(bytes);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showDismissibleSnackBar(
|
||||
@@ -300,6 +313,19 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_pendingOperations.add(ContactOperationType.import);
|
||||
try {
|
||||
await connector.sendFrame(importContactFrame, expectsGenericAck: true);
|
||||
} catch (e) {
|
||||
_pendingOperations.remove(ContactOperationType.import);
|
||||
if (mounted) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactImportFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +348,34 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
tooltip: context.l10n.contacts_moreOptions,
|
||||
itemBuilder: (context) => <PopupMenuEntry<dynamic>>[
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person_add_rounded),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.discoveredContacts_Title),
|
||||
],
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DiscoveryScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.paste),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.contacts_addContactFromClipboard),
|
||||
],
|
||||
),
|
||||
onTap: () => _contactImport(),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -365,46 +418,20 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
),
|
||||
onTap: () => _contactExport(Uint8List.fromList([])),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.paste),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.contacts_addContactFromClipboard),
|
||||
],
|
||||
),
|
||||
onTap: () => _contactImport(),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.connect_without_contact),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout, color: Colors.red),
|
||||
Icon(
|
||||
Icons.logout,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.common_disconnect),
|
||||
],
|
||||
),
|
||||
onTap: () => _disconnect(context, connector),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person_add_rounded),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.discoveredContacts_Title),
|
||||
],
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DiscoveryScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -426,6 +453,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
],
|
||||
),
|
||||
body: _buildContactsBody(context, connector),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showAddContactSheet(context),
|
||||
child: const Icon(Icons.person_add),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: QuickSwitchBar(
|
||||
@@ -440,6 +471,40 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddContactSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.paste),
|
||||
title: Text(context.l10n.contacts_addContactFromClipboard),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_contactImport();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add_rounded),
|
||||
title: Text(context.l10n.discoveredContacts_Title),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DiscoveryScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _disconnect(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
@@ -571,7 +636,11 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
tooltip: menuContext.l10n.contacts_deleteGroup,
|
||||
icon: const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: canManageGroups
|
||||
? () => _closeDropdownAndRun(
|
||||
menuContext,
|
||||
@@ -589,16 +658,25 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
],
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(selectedGroupName, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedGroupName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -624,6 +702,16 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
icon: Icons.people_outline,
|
||||
title: context.l10n.contacts_noContacts,
|
||||
subtitle: context.l10n.contacts_contactsWillAppear,
|
||||
action: FilledButton.icon(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DiscoveryScreen(),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.person_add_rounded),
|
||||
label: Text(context.l10n.discoveredContacts_Title),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -759,6 +847,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: IconButton(
|
||||
tooltip: viewState.contactsSearchExpanded
|
||||
? context.l10n.contacts_searchClose
|
||||
: context.l10n.contacts_searchOpen,
|
||||
onPressed: () {
|
||||
if (viewState.contactsSearchExpanded) {
|
||||
_collapseContactsSearch(viewState);
|
||||
@@ -791,25 +882,29 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: filteredAndSorted.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
viewState.contactsShowUnreadOnly
|
||||
? context.l10n.contacts_noUnreadContacts
|
||||
: context.l10n.contacts_noContactsFound,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () => connector.getContacts(),
|
||||
child: ListView.builder(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => connector.getContacts(),
|
||||
child: filteredAndSorted.isEmpty
|
||||
? LayoutBuilder(
|
||||
builder: (context, constraints) => ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: EmptyState(
|
||||
icon: Icons.search_off,
|
||||
title: viewState.contactsShowUnreadOnly
|
||||
? context.l10n.contacts_noUnreadContacts
|
||||
: context.l10n.contacts_noContactsFound,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 88),
|
||||
itemCount: filteredAndSorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = filteredAndSorted[index];
|
||||
@@ -827,7 +922,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -1048,7 +1143,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.common_delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -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(
|
||||
leading: Icon(
|
||||
@@ -1403,10 +1490,15 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.red),
|
||||
leading: Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.contacts_deleteContact,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
@@ -1441,7 +1533,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.common_delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1473,25 +1565,14 @@ class _ContactTile extends StatelessWidget {
|
||||
onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null,
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
backgroundColor: contactTypeColor(contact.type),
|
||||
child: _buildContactAvatar(contact),
|
||||
),
|
||||
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
contact.pathLabel(context.l10n),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
contact.shortPubKeyHex,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
subtitle: Text(
|
||||
contact.pathLabel(context.l10n),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// Clamp text scaling in trailing section to prevent overflow while
|
||||
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||
@@ -1502,7 +1583,7 @@ class _ContactTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
width: 96,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
@@ -1516,7 +1597,10 @@ class _ContactTile extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1529,7 +1613,9 @@ class _ContactTile extends StatelessWidget {
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1548,37 +1634,7 @@ class _ContactTile extends StatelessWidget {
|
||||
if (emoji != null) {
|
||||
return Text(emoji, style: const TextStyle(fontSize: 18));
|
||||
}
|
||||
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Icons.chat;
|
||||
case advTypeRepeater:
|
||||
return Icons.cell_tower;
|
||||
case advTypeRoom:
|
||||
return Icons.group;
|
||||
case advTypeSensor:
|
||||
return Icons.sensors;
|
||||
default:
|
||||
return Icons.device_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getTypeColor(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Colors.blue;
|
||||
case advTypeRepeater:
|
||||
return Colors.orange;
|
||||
case advTypeRoom:
|
||||
return Colors.purple;
|
||||
case advTypeSensor:
|
||||
return Colors.green;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
return Icon(contactTypeIcon(contact.type), color: Colors.white, size: 20);
|
||||
}
|
||||
|
||||
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../utils/contact_search.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../widgets/app_bar.dart';
|
||||
import '../widgets/list_filter_widget.dart';
|
||||
import '../helpers/contact_ui.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
enum DiscoverySortOption { lastSeen, name, type }
|
||||
@@ -71,7 +72,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, color: Colors.red),
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.discoveredContacts_deleteContactAll),
|
||||
],
|
||||
@@ -99,9 +103,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
final contact = filteredAndSorted[index];
|
||||
final tile = ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
backgroundColor: contactTypeColor(contact.type),
|
||||
child: Icon(
|
||||
_getTypeIcon(contact.type),
|
||||
contactTypeIcon(contact.type),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
@@ -142,7 +146,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
@@ -152,7 +158,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
if (contact.rawPacket != null)
|
||||
const SizedBox(width: 2),
|
||||
@@ -160,7 +169,10 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
Icon(
|
||||
Icons.cell_tower,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -170,6 +182,17 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
),
|
||||
onTap: () {
|
||||
connector.importDiscoveredContact(contact);
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.discoveredContacts_contactAdded,
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: context.l10n.common_undo,
|
||||
onPressed: () =>
|
||||
connector.removeContact(contact),
|
||||
),
|
||||
);
|
||||
},
|
||||
onLongPress: () =>
|
||||
_showContactContextMenu(contact, connector),
|
||||
@@ -203,11 +226,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_reaction_sharp),
|
||||
title: Text(l10n.discoveredContacts_addContact),
|
||||
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: Text(l10n.discoveredContacts_copyContact),
|
||||
@@ -227,9 +245,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
if (!mounted || action == null) return;
|
||||
|
||||
switch (action) {
|
||||
case 'import_contact':
|
||||
connector.importDiscoveredContact(contact);
|
||||
break;
|
||||
case 'copy_contact':
|
||||
if (contact.rawPacket == null) return;
|
||||
final hexString = pubKeyToHex(contact.rawPacket!);
|
||||
@@ -429,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) {
|
||||
final now = DateTime.now();
|
||||
|
||||
@@ -73,7 +73,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
double _startAntennaHeight = 5.0;
|
||||
double _endAntennaHeight = 5.0;
|
||||
bool _showHud = true;
|
||||
bool _menuExpanded = true;
|
||||
bool _menuExpanded = false;
|
||||
bool _showDisplayNodes = true;
|
||||
bool _showMarkerLabels = true;
|
||||
bool _didReceivePositionUpdate = false;
|
||||
@@ -159,17 +159,17 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
@@ -224,6 +224,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
setState(() {
|
||||
_result = result;
|
||||
_selectedObstruction = _defaultObstructionFor(result);
|
||||
_menuExpanded = true;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -506,7 +507,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
bottom: 12,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
@@ -516,11 +517,18 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.losElevationAttribution,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.white),
|
||||
style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_loading)
|
||||
const Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@@ -623,7 +631,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.losBlockedSpotsHint,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
|
||||
style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
@@ -692,7 +700,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
'${_selectedObstruction!.point.longitude.toStringAsFixed(5)}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[700],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -711,14 +719,14 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
context.l10n.losFrequencyLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[700],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${displayFrequencyMHz.toStringAsFixed(3)} MHz',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
|
||||
style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
if (kFactorUsed != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
@@ -726,7 +734,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
'k=${kFactorUsed.toStringAsFixed(3)}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[700],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -734,7 +742,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(Icons.info_outline, size: 16),
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
tooltip: context.l10n.losFrequencyInfoTooltip,
|
||||
onPressed: () {
|
||||
_showFrequencyInfoDialog(
|
||||
@@ -750,7 +758,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
Text(
|
||||
context.l10n.losElevationAttribution,
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
|
||||
style: TextStyle(fontSize: 10, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ExpansionTile(
|
||||
@@ -1730,12 +1738,12 @@ class _LosLegend extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle =
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
) ??
|
||||
const TextStyle(
|
||||
color: Colors.white70,
|
||||
TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
|
||||
@@ -83,17 +83,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
@@ -458,7 +458,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
l10n.mapCache_failedDownloads(_failedTiles),
|
||||
style: TextStyle(color: Colors.orange[700]),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+202
-169
@@ -183,17 +183,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: () => _mapController.move(center, zoom),
|
||||
),
|
||||
],
|
||||
@@ -417,61 +417,76 @@ class _MapScreenState extends State<MapScreen> {
|
||||
automaticallyImplyLeading: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
if (!_isBuildingPathTrace)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.radar),
|
||||
onPressed: () => _startPath(
|
||||
LatLng(connector.selfLatitude!, connector.selfLongitude!),
|
||||
),
|
||||
tooltip: context.l10n.contacts_pathTrace,
|
||||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
IconButton(
|
||||
icon: const LosIcon(),
|
||||
onPressed: () {
|
||||
final candidates = <LineOfSightEndpoint>[];
|
||||
if (connector.selfLatitude != null &&
|
||||
connector.selfLongitude != null) {
|
||||
candidates.add(
|
||||
LineOfSightEndpoint(
|
||||
label: context.l10n.pathTrace_you,
|
||||
point: LatLng(
|
||||
connector.selfLatitude!,
|
||||
connector.selfLongitude!,
|
||||
),
|
||||
color: Colors.teal,
|
||||
icon: Icons.person_pin_circle,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final c in contactsWithLocation) {
|
||||
candidates.add(
|
||||
LineOfSightEndpoint(
|
||||
label: c.name,
|
||||
point: LatLng(c.latitude!, c.longitude!),
|
||||
color: _getNodeColor(c.type),
|
||||
icon: _getNodeIcon(c.type),
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LineOfSightMapScreen(
|
||||
title: context.l10n.map_losScreenTitle,
|
||||
candidates: candidates,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: context.l10n.map_lineOfSight,
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
if (!_isBuildingPathTrace &&
|
||||
connector.selfLatitude != null &&
|
||||
connector.selfLongitude != null)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.radar),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.contacts_pathTrace),
|
||||
],
|
||||
),
|
||||
onTap: () => _startPath(
|
||||
LatLng(
|
||||
connector.selfLatitude!,
|
||||
connector.selfLongitude!,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const LosIcon(),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.map_lineOfSight),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final candidates = <LineOfSightEndpoint>[];
|
||||
if (connector.selfLatitude != null &&
|
||||
connector.selfLongitude != null) {
|
||||
candidates.add(
|
||||
LineOfSightEndpoint(
|
||||
label: context.l10n.pathTrace_you,
|
||||
point: LatLng(
|
||||
connector.selfLatitude!,
|
||||
connector.selfLongitude!,
|
||||
),
|
||||
color: Colors.teal,
|
||||
icon: Icons.person_pin_circle,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final c in contactsWithLocation) {
|
||||
candidates.add(
|
||||
LineOfSightEndpoint(
|
||||
label: c.name,
|
||||
point: LatLng(c.latitude!, c.longitude!),
|
||||
color: _getNodeColor(c.type),
|
||||
icon: _getNodeIcon(c.type),
|
||||
),
|
||||
);
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LineOfSightMapScreen(
|
||||
title: context.l10n.map_losScreenTitle,
|
||||
candidates: candidates,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout, color: Colors.red),
|
||||
Icon(Icons.logout, color: Theme.of(context).colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.common_disconnect),
|
||||
],
|
||||
@@ -906,8 +921,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final color = _getNodeColor(guess.contact.type);
|
||||
final marker = Marker(
|
||||
point: guess.position,
|
||||
width: 35,
|
||||
height: 35,
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _isBuildingPathTrace
|
||||
? _showNodeInfo(context, guess.contact)
|
||||
@@ -919,26 +934,28 @@ class _MapScreenState extends State<MapScreen> {
|
||||
guess.contact,
|
||||
guessedPosition: guess.position,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(
|
||||
alpha: guess.highConfidence ? 0.55 : 0.30,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(
|
||||
alpha: guess.highConfidence ? 0.55 : 0.30,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.not_listed_location,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.not_listed_location,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1030,39 +1047,37 @@ class _MapScreenState extends State<MapScreen> {
|
||||
for (final contact in filteredContacts) {
|
||||
final marker = Marker(
|
||||
point: LatLng(contact.latitude!, contact.longitude!),
|
||||
width: 35,
|
||||
height: 35,
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
onLongPress: () =>
|
||||
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
|
||||
onTap: () => _isBuildingPathTrace
|
||||
? _addToPath(context, contact)
|
||||
: _showNodeInfo(context, contact),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: settings.mapShowOverlaps && !_isBuildingPathTrace
|
||||
? Colors.red
|
||||
: _getNodeColor(contact.type),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
_getNodeIcon(contact.type),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: settings.mapShowOverlaps && !_isBuildingPathTrace
|
||||
? Colors.red
|
||||
: _getNodeColor(contact.type),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: Icon(
|
||||
_getNodeIcon(contact.type),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1208,7 +1223,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
": $nodeCount",
|
||||
@@ -1221,10 +1236,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.wrong_location,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
": ${nodeCountAll - nodeCount}",
|
||||
@@ -1237,10 +1252,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.add_outlined,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
": $nodeCountAll",
|
||||
@@ -1536,56 +1551,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
LatLng? guessedPosition,
|
||||
}) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
showDialog(
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getNodeIcon(contact.type),
|
||||
color: _getNodeColor(contact.type),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: SelectableText(contact.name)),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context.l10n.map_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
context.l10n.map_location,
|
||||
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
|
||||
)
|
||||
else if (guessedPosition != null)
|
||||
_buildInfoRow(
|
||||
context.l10n.map_estLocation,
|
||||
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_lastSeen,
|
||||
_formatLastSeen(contact.lastSeen),
|
||||
),
|
||||
_buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
if (contact.type ==
|
||||
advTypeChat) // Only show chat button for chat nodes
|
||||
TextButton(
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) {
|
||||
final actions = <Widget>[];
|
||||
if (contact.type == advTypeChat) {
|
||||
actions.add(
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (!contact.isActive) {
|
||||
connector.importDiscoveredContact(contact);
|
||||
@@ -1593,7 +1566,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.pop(sheetContext);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -1606,30 +1579,88 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
child: Text(context.l10n.contacts_openChat),
|
||||
),
|
||||
if (contact.type == advTypeRepeater)
|
||||
TextButton(
|
||||
);
|
||||
}
|
||||
if (contact.type == advTypeRepeater) {
|
||||
actions.add(
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (!contact.isActive) {
|
||||
connector.importDiscoveredContact(contact);
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.pop(sheetContext);
|
||||
_showRepeaterLogin(context, contact);
|
||||
},
|
||||
child: Text(context.l10n.map_manageRepeater),
|
||||
),
|
||||
if (contact.type == advTypeRoom)
|
||||
TextButton(
|
||||
);
|
||||
}
|
||||
if (contact.type == advTypeRoom) {
|
||||
actions.add(
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (!contact.isActive) {
|
||||
connector.importDiscoveredContact(contact);
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.pop(sheetContext);
|
||||
_showRoomLogin(context, contact);
|
||||
},
|
||||
child: Text(context.l10n.map_joinRoom),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getNodeIcon(contact.type),
|
||||
color: _getNodeColor(contact.type),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: SelectableText(contact.name)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
context.l10n.map_location,
|
||||
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
|
||||
)
|
||||
else if (guessedPosition != null)
|
||||
_buildInfoRow(
|
||||
context.l10n.map_estLocation,
|
||||
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_lastSeen,
|
||||
_formatLastSeen(contact.lastSeen),
|
||||
),
|
||||
_buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
|
||||
const SizedBox(height: 16),
|
||||
...actions,
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(sheetContext),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1714,6 +1745,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: Text(context.l10n.common_hide),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_hiddenMarkerIds.add(marker.id);
|
||||
@@ -1745,7 +1779,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -1810,9 +1844,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
messenger.context,
|
||||
content: Text(successMsg),
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(successMsg)),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -2202,7 +2235,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_getTimeFilterLabel(settings.mapTimeFilterHours),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Slider(
|
||||
value: _hoursToSliderValue(settings.mapTimeFilterHours),
|
||||
@@ -2354,7 +2387,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (_pathTrace.isNotEmpty)
|
||||
Text(
|
||||
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
SelectableText(
|
||||
_pathTrace
|
||||
|
||||
@@ -9,7 +9,8 @@ import '../models/path_selection.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/routing_sheet.dart';
|
||||
import '../widgets/snr_indicator.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
@@ -167,7 +168,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
backgroundColor: Theme.of(context).colorScheme.tertiary,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
@@ -227,7 +228,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_requestTimedOut),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
@@ -241,7 +242,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -279,7 +280,9 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
children: [
|
||||
Text(
|
||||
l10n.neighbors_repeatersNeighbors,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
repeater.name,
|
||||
@@ -287,75 +290,18 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
IconButton(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
ContactRoutingSheet.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
@@ -380,11 +326,9 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
if (!_isLoaded &&
|
||||
!_hasData &&
|
||||
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||
Center(
|
||||
child: Text(
|
||||
l10n.neighbors_noData,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
EmptyState(
|
||||
icon: Icons.wifi_find,
|
||||
title: l10n.neighbors_noData,
|
||||
),
|
||||
if (_isLoaded ||
|
||||
_hasData &&
|
||||
@@ -435,7 +379,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
fmtDuration(entry.value['lastHeard'] + 0.0),
|
||||
),
|
||||
entry.value['snr'],
|
||||
connector.currentSf!,
|
||||
connector.currentSf,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -447,7 +391,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
String label,
|
||||
String value,
|
||||
double snr,
|
||||
int spreadingFactor,
|
||||
int? spreadingFactor,
|
||||
) {
|
||||
final snrUi = snrUiFromSNR(snr, spreadingFactor);
|
||||
return Padding(
|
||||
|
||||
+136
-78
@@ -103,6 +103,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
double _pathDistanceMeters = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
Contact? _targetContact;
|
||||
// Live path resolved at trace time; used by the response handler for
|
||||
// endpoint inference so it matches the path that was actually traced.
|
||||
Uint8List _tracedPath = Uint8List(0);
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
@@ -168,17 +171,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
@@ -228,6 +231,23 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
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 {
|
||||
if (mounted) {
|
||||
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
|
||||
? Uint8List.fromList(widget.path.reversed.toList())
|
||||
: widget.path;
|
||||
? Uint8List.fromList(livePath.reversed.toList())
|
||||
: livePath;
|
||||
|
||||
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
|
||||
|
||||
@@ -248,7 +272,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
noNotify: !mounted,
|
||||
);
|
||||
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final frame = buildTraceReq(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
0, //flags
|
||||
@@ -414,13 +437,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
final tc = _targetContact!;
|
||||
if (tc.hasLocation) {
|
||||
targetPos = LatLng(tc.latitude!, tc.longitude!);
|
||||
} else if (widget.path.length > 1) {
|
||||
} else if (_tracedPath.length > 1) {
|
||||
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
|
||||
// sits in the middle of the symmetric sequence; .last is the local side.
|
||||
final lastHop = widget.reversePathAround
|
||||
? widget.path.first
|
||||
: widget.path.last;
|
||||
? _tracedPath.first
|
||||
: _tracedPath.last;
|
||||
|
||||
final peers = connector.allContacts
|
||||
.where(
|
||||
@@ -593,7 +616,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
!_failed2Loaded)
|
||||
Center(
|
||||
child: Card(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Text(
|
||||
@@ -640,31 +663,35 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
markers.add(
|
||||
Marker(
|
||||
point: point,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: hasGps
|
||||
? Colors.green
|
||||
: Colors.orange.withValues(alpha: 0.75),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: hasGps
|
||||
? Colors.green
|
||||
: Colors.orange.withValues(alpha: 0.75),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hasGps ? label : '~$label',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hasGps ? label : '~$label',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -689,29 +716,33 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
markers.add(
|
||||
Marker(
|
||||
point: selfPoint,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.l10n.pathTrace_you,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.l10n.pathTrace_you,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -735,26 +766,30 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
markers.add(
|
||||
Marker(
|
||||
point: targetPos,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: isGuessed
|
||||
? Colors.purple.withValues(alpha: 0.55)
|
||||
: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: isGuessed
|
||||
? Colors.purple.withValues(alpha: 0.55)
|
||||
: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(Icons.person, color: Colors.white, size: 18),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(Icons.person, color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -927,6 +962,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _colorDot(Color color) => Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
);
|
||||
|
||||
Widget _buildLegendCard(
|
||||
BuildContext context,
|
||||
PathTraceData pathTraceData,
|
||||
@@ -949,9 +990,26 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
_colorDot(Colors.green),
|
||||
const SizedBox(width: 4),
|
||||
Text(l10n.pathTrace_legendGpsConfirmed, style: const TextStyle(fontSize: 11)),
|
||||
const SizedBox(width: 12),
|
||||
_colorDot(Colors.orange),
|
||||
const SizedBox(width: 4),
|
||||
Text(l10n.pathTrace_legendInferred, style: const TextStyle(fontSize: 11)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
@@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../widgets/debug_frame_viewer.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/routing_sheet.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterCliScreen extends StatefulWidget {
|
||||
@@ -252,97 +252,29 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(l10n.repeater_cliTitle),
|
||||
Text(
|
||||
l10n.repeater_cliTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
IconButton(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
tooltip: l10n.repeater_debugNextCommand,
|
||||
onPressed: () {
|
||||
// Set a flag or just send next command with debug
|
||||
if (_commandController.text.trim().isNotEmpty) {
|
||||
_sendCommand(showDebug: true);
|
||||
} else {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_enterCommandFirst),
|
||||
);
|
||||
}
|
||||
},
|
||||
ContactRoutingSheet.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
@@ -354,6 +286,33 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
tooltip: l10n.repeater_clearHistory,
|
||||
onPressed: _commandHistory.isEmpty ? null : _clearHistory,
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
if (value == 'debug') {
|
||||
if (_commandController.text.trim().isNotEmpty) {
|
||||
_sendCommand(showDebug: true);
|
||||
} else {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_enterCommandFirst),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'debug',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.bug_report),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.repeater_debugNextCommand),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
@@ -426,16 +385,16 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.terminal, size: 64, color: Colors.grey[400]),
|
||||
Icon(Icons.terminal, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.repeater_noCommandsSent,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.repeater_typeCommandOrUseQuick,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -72,11 +72,11 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.orange,
|
||||
child: const Icon(
|
||||
backgroundColor: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
child: Icon(
|
||||
Icons.cell_tower,
|
||||
size: 40,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -90,12 +90,12 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
repeater.shortPubKeyHex,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
repeater.pathLabel(context.l10n),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
if (repeater.hasLocation) ...[
|
||||
const SizedBox(height: 4),
|
||||
@@ -105,14 +105,14 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -193,7 +193,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
icon: Icons.analytics,
|
||||
title: l10n.repeater_status,
|
||||
subtitle: l10n.repeater_statusSubtitle,
|
||||
color: Colors.blue,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -213,7 +213,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
icon: Icons.bar_chart_sharp,
|
||||
title: l10n.repeater_telemetry,
|
||||
subtitle: l10n.repeater_telemetrySubtitle,
|
||||
color: Colors.teal,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -231,7 +231,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
icon: Icons.terminal,
|
||||
title: l10n.repeater_cli,
|
||||
subtitle: l10n.repeater_cliSubtitle,
|
||||
color: Colors.green,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -251,7 +251,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
icon: Icons.group,
|
||||
title: l10n.repeater_neighbors,
|
||||
subtitle: l10n.repeater_neighborsSubtitle,
|
||||
color: Colors.orange,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -270,7 +270,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
icon: Icons.settings,
|
||||
title: l10n.repeater_settings,
|
||||
subtitle: l10n.repeater_settingsSubtitle,
|
||||
color: Colors.deepOrange,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -329,12 +329,12 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: Colors.grey[400]),
|
||||
Icon(Icons.chevron_right, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/routing_sheet.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterSettingsScreen extends StatefulWidget {
|
||||
@@ -126,6 +126,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
// Location settings
|
||||
final TextEditingController _latController = TextEditingController();
|
||||
final TextEditingController _lonController = TextEditingController();
|
||||
bool _latInvalid = false;
|
||||
bool _lonInvalid = false;
|
||||
|
||||
// Feature toggles
|
||||
bool _repeatEnabled = true;
|
||||
@@ -457,7 +459,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
? l10n.repeater_refreshed(label)
|
||||
: l10n.repeater_errorRefreshing(label),
|
||||
),
|
||||
backgroundColor: successCount > 0 ? Colors.green : Colors.red,
|
||||
backgroundColor: successCount > 0 ? null : Theme.of(context).colorScheme.error,
|
||||
);
|
||||
setState(() => setRefreshing(false));
|
||||
}
|
||||
@@ -667,15 +669,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
: l10n.repeater_actionSucceeded(label),
|
||||
),
|
||||
backgroundColor: outcome == _SaveOutcome.error
|
||||
? Colors.red
|
||||
: Colors.green,
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_actionFailed(label, e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _runningAction = false);
|
||||
@@ -768,14 +770,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
}
|
||||
|
||||
if (_dirtyFields.contains(_SettingField.lat) &&
|
||||
_latController.text.isNotEmpty) {
|
||||
_latController.text.isNotEmpty &&
|
||||
_isValidCoordinate(_latController.text, 90)) {
|
||||
pending.add((
|
||||
field: _SettingField.lat,
|
||||
command: 'set lat ${_latController.text}',
|
||||
));
|
||||
}
|
||||
if (_dirtyFields.contains(_SettingField.lon) &&
|
||||
_lonController.text.isNotEmpty) {
|
||||
_lonController.text.isNotEmpty &&
|
||||
_isValidCoordinate(_lonController.text, 180)) {
|
||||
pending.add((
|
||||
field: _SettingField.lon,
|
||||
command: 'set lon ${_lonController.text}',
|
||||
@@ -944,13 +948,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_settingsSavedRebootNeeded),
|
||||
backgroundColor: Colors.orange,
|
||||
backgroundColor: Theme.of(context).colorScheme.tertiary,
|
||||
);
|
||||
} else if (failures.isEmpty) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_settingsSaved),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
} else {
|
||||
showDismissibleSnackBar(
|
||||
@@ -958,7 +961,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
content: Text(
|
||||
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(
|
||||
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();
|
||||
}
|
||||
|
||||
static bool _isValidCoordinate(String text, double max) {
|
||||
if (text.trim().isEmpty) return true;
|
||||
final value = double.tryParse(text.trim());
|
||||
return value != null && value >= -max && value <= max;
|
||||
}
|
||||
|
||||
void _flagHasChanges() {
|
||||
if (!_hasChanges) {
|
||||
setState(() {
|
||||
@@ -1072,73 +1081,11 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
IconButton(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
ContactRoutingSheet.show(context, contact: repeater),
|
||||
),
|
||||
if (_hasChanges)
|
||||
TextButton.icon(
|
||||
@@ -1173,6 +1120,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
const SizedBox(height: 16),
|
||||
_buildAdvancedCard(),
|
||||
const SizedBox(height: 32),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
_buildDangerZoneCard(),
|
||||
],
|
||||
),
|
||||
@@ -1388,13 +1337,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_latitude,
|
||||
helperText: l10n.repeater_latitudeHelper,
|
||||
errorText: _latInvalid
|
||||
? l10n.settings_locationInvalid
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
onChanged: (_) => _markChanged(_SettingField.lat),
|
||||
onChanged: (value) {
|
||||
_markChanged(_SettingField.lat);
|
||||
final invalid = !_isValidCoordinate(value, 90);
|
||||
if (invalid != _latInvalid) {
|
||||
setState(() => _latInvalid = invalid);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -1415,13 +1373,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_longitude,
|
||||
helperText: l10n.repeater_longitudeHelper,
|
||||
errorText: _lonInvalid
|
||||
? l10n.settings_locationInvalid
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
onChanged: (_) => _markChanged(_SettingField.lon),
|
||||
onChanged: (value) {
|
||||
_markChanged(_SettingField.lon);
|
||||
final invalid = !_isValidCoordinate(value, 180);
|
||||
if (invalid != _lonInvalid) {
|
||||
setState(() => _lonInvalid = invalid);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -2233,7 +2200,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2262,7 +2229,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
onConfirm();
|
||||
},
|
||||
style: isDestructive
|
||||
? FilledButton.styleFrom(backgroundColor: Colors.red)
|
||||
? FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error)
|
||||
: null,
|
||||
child: Text(l10n.repeater_confirm),
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/routing_sheet.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterStatusScreen extends StatefulWidget {
|
||||
@@ -318,7 +318,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_statusRequestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
@@ -331,7 +331,7 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
_recordStatusResult(false);
|
||||
@@ -360,82 +360,29 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(l10n.repeater_statusTitle),
|
||||
Text(
|
||||
l10n.repeater_statusTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
IconButton(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
ContactRoutingSheet.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
@@ -588,21 +535,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 130,
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../services/linux_ble_error_classifier.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/device_tile.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
@@ -25,6 +26,7 @@ class ScannerScreen extends StatefulWidget {
|
||||
|
||||
class _ScannerScreenState extends State<ScannerScreen> {
|
||||
bool _changedNavigation = false;
|
||||
String? _connectingDeviceId;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||
@@ -101,6 +103,32 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
if (PlatformInfo.supportsUsbSerial)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.usb),
|
||||
tooltip: context.l10n.connectionChoiceUsbLabel,
|
||||
onPressed: () {
|
||||
appLogger.info(
|
||||
'USB selected, opening UsbScreen',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!PlatformInfo.isWeb)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.lan),
|
||||
tooltip: context.l10n.connectionChoiceTcpLabel,
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
@@ -122,84 +150,24 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||
floatingActionButton: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isScanning =
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||
final usbSupported = PlatformInfo.supportsUsbSerial;
|
||||
final tcpSupported = !PlatformInfo.isWeb;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (usbSupported)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
appLogger.info(
|
||||
'USB selected, opening UsbScreen',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'scanner_usb_action',
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
if (usbSupported) const SizedBox(width: 12),
|
||||
if (tcpSupported)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'scanner_tcp_action',
|
||||
icon: const Icon(Icons.lan),
|
||||
label: Text(context.l10n.connectionChoiceTcpLabel),
|
||||
),
|
||||
if (tcpSupported) const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
heroTag: 'scanner_ble_action',
|
||||
onPressed: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
unawaited(
|
||||
connector.startScan().catchError((e) {
|
||||
appLogger.warn(
|
||||
'startScan error: $e',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(
|
||||
isScanning
|
||||
? context.l10n.scanner_stop
|
||||
: context.l10n.scanner_scan,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return FloatingActionButton.extended(
|
||||
heroTag: 'scanner_ble_action',
|
||||
onPressed: isBluetoothOff ? null : () => _toggleScan(connector),
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(
|
||||
isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -207,6 +175,18 @@ class _ScannerScreenState extends State<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) {
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
@@ -254,32 +234,43 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
|
||||
Widget _buildDeviceList(BuildContext context, MeshCoreConnector connector) {
|
||||
if (connector.scanResults.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
connector.state == MeshCoreConnectionState.scanning
|
||||
? context.l10n.scanner_searchingDevices
|
||||
: context.l10n.scanner_tapToScan,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||
final isScanning = connector.state == MeshCoreConnectionState.scanning;
|
||||
return EmptyState(
|
||||
icon: isBluetoothOff ? Icons.bluetooth_disabled : Icons.bluetooth,
|
||||
title: isBluetoothOff
|
||||
? context.l10n.scanner_bluetoothOff
|
||||
: isScanning
|
||||
? context.l10n.scanner_searchingDevices
|
||||
: context.l10n.scanner_tapToScan,
|
||||
subtitle: isBluetoothOff
|
||||
? context.l10n.scanner_bluetoothOffMessage
|
||||
: null,
|
||||
action: (isBluetoothOff || isScanning)
|
||||
? null
|
||||
: FilledButton.icon(
|
||||
onPressed: () => _toggleScan(connector),
|
||||
icon: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(context.l10n.scanner_scan),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isConnecting =
|
||||
connector.state == MeshCoreConnectionState.connecting;
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: connector.scanResults.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final result = connector.scanResults[index];
|
||||
final deviceId = result.device.remoteId.toString();
|
||||
return DeviceTile(
|
||||
scanResult: result,
|
||||
onTap: () => _connectToDevice(context, connector, result),
|
||||
isConnecting: isConnecting && _connectingDeviceId == deviceId,
|
||||
onTap: isConnecting
|
||||
? null
|
||||
: () => _connectToDevice(context, connector, result),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -293,6 +284,9 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
final name = result.device.platformName.isNotEmpty
|
||||
? result.device.platformName
|
||||
: result.advertisementData.advName;
|
||||
setState(() {
|
||||
_connectingDeviceId = result.device.remoteId.toString();
|
||||
});
|
||||
try {
|
||||
await connector.connect(
|
||||
result.device,
|
||||
@@ -321,9 +315,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_connectingDeviceId = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
Future<void> _loadVersionInfo() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_appVersion = packageInfo.version;
|
||||
});
|
||||
@@ -213,12 +214,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
if (percent == null) {
|
||||
icon = Icons.battery_unknown;
|
||||
iconColor = Colors.grey;
|
||||
iconColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
valueColor = null;
|
||||
} else if (percent <= 15) {
|
||||
icon = Icons.battery_alert;
|
||||
iconColor = Colors.orange;
|
||||
valueColor = Colors.orange;
|
||||
iconColor = Theme.of(context).colorScheme.tertiary;
|
||||
valueColor = Theme.of(context).colorScheme.tertiary;
|
||||
} else {
|
||||
icon = Icons.battery_full;
|
||||
iconColor = null;
|
||||
@@ -307,18 +308,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _editLocation(context, connector),
|
||||
),
|
||||
if (connector.currentCustomVars?.containsKey('gps') ?? false) ...[
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.gps_fixed),
|
||||
title: Text(l10n.settings_locationGPSEnable),
|
||||
subtitle: Text(l10n.settings_locationGPSEnableSubtitle),
|
||||
value: connector.currentCustomVars?['gps'] == '1',
|
||||
onChanged: (value) async {
|
||||
await connector.setCustomVar(value ? 'gps:1' : 'gps:0');
|
||||
},
|
||||
),
|
||||
],
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.group_add_outlined),
|
||||
@@ -354,13 +343,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
leading: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error),
|
||||
title: Text(l10n.settings_deleteAllPaths),
|
||||
subtitle: Text(
|
||||
l10n.settings_deleteAllPathsSubtitle,
|
||||
style: TextStyle(color: Colors.red[700]),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
onTap: () => connector.deleteAllPaths(),
|
||||
onTap: () => _confirmDeleteAllPaths(context, connector),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
@@ -378,7 +367,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.restart_alt, color: Colors.orange),
|
||||
leading: Icon(Icons.restart_alt, color: Theme.of(context).colorScheme.tertiary),
|
||||
title: Text(l10n.settings_rebootDevice),
|
||||
subtitle: Text(l10n.settings_rebootDeviceSubtitle),
|
||||
onTap: () => _confirmReboot(context, connector),
|
||||
@@ -565,6 +554,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
||||
intervalController.text = currentInterval.toString();
|
||||
|
||||
String? intervalError;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
@@ -600,9 +591,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: intervalController,
|
||||
onChanged: (_) {
|
||||
if (intervalError != null) {
|
||||
setDialogState(() => intervalError = null);
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_locationIntervalSec,
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: intervalError,
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: false,
|
||||
@@ -633,24 +630,25 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
|
||||
int? interval;
|
||||
if (hasGPS) {
|
||||
final intervalText = intervalController.text.trim();
|
||||
if (intervalText.isEmpty) {
|
||||
return;
|
||||
if (intervalText.isNotEmpty) {
|
||||
interval = int.tryParse(intervalText);
|
||||
if (interval == null ||
|
||||
interval < 60 ||
|
||||
interval >= 86400) {
|
||||
setDialogState(() {
|
||||
intervalError = l10n.settings_locationIntervalInvalid;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final interval = int.tryParse(intervalText);
|
||||
if (interval == null || interval < 60 || interval >= 86400) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_locationIntervalInvalid),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.pop(context);
|
||||
|
||||
if (interval != null) {
|
||||
await connector.setCustomVar("gps_interval:$interval");
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
@@ -716,6 +714,36 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDeleteAllPaths(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.settings_deleteAllPaths),
|
||||
content: Text(l10n.settings_deleteAllPathsSubtitle),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
connector.deleteAllPaths();
|
||||
},
|
||||
child: Text(
|
||||
l10n.common_deleteAll,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
showDialog(
|
||||
@@ -735,7 +763,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
child: Text(
|
||||
l10n.common_reboot,
|
||||
style: const TextStyle(color: Colors.orange),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1126,6 +1154,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
bool _clientRepeat = false;
|
||||
int? _selectedPresetIndex;
|
||||
_RadioSettingsSnapshot? _lastNonRepeatSnapshot;
|
||||
String? _frequencyError;
|
||||
String? _txPowerError;
|
||||
|
||||
AppDebugLogService get _appLog =>
|
||||
Provider.of<AppDebugLogService>(context, listen: false);
|
||||
@@ -1392,7 +1422,24 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
|
||||
void _handleManualSettingsChanged(String source) {
|
||||
_logRadioSettingsState('Manual settings edit: $source');
|
||||
setState(_syncPresetSelection);
|
||||
setState(() {
|
||||
_validateFields();
|
||||
_syncPresetSelection();
|
||||
});
|
||||
}
|
||||
|
||||
void _validateFields() {
|
||||
final l10n = context.l10n;
|
||||
final freqMHz = double.tryParse(_frequencyController.text);
|
||||
_frequencyError = (freqMHz == null || freqMHz < 300 || freqMHz > 2500)
|
||||
? l10n.settings_frequencyInvalid
|
||||
: null;
|
||||
|
||||
final maxTxPower = widget.connector.maxTxPower ?? 22;
|
||||
final txPower = int.tryParse(_txPowerController.text);
|
||||
_txPowerError = (txPower == null || txPower < 0 || txPower > maxTxPower)
|
||||
? '${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'
|
||||
: null;
|
||||
}
|
||||
|
||||
void _handleClientRepeatChanged(bool enabled) {
|
||||
@@ -1504,6 +1551,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
content: Text(l10n.settings_error(e.toString())),
|
||||
);
|
||||
}
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@@ -1579,6 +1627,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
labelText: l10n.settings_frequency,
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: l10n.settings_frequencyHelper,
|
||||
errorText: _frequencyError,
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
@@ -1662,6 +1711,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
helperText: widget.connector.maxTxPower != null
|
||||
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
|
||||
: l10n.settings_txPowerHelper,
|
||||
errorText: _txPowerError,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
@@ -1683,7 +1733,12 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
|
||||
FilledButton(
|
||||
onPressed: (_frequencyError != null || _txPowerError != null)
|
||||
? null
|
||||
: _saveSettings,
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+39
-47
@@ -95,12 +95,14 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
final isConnecting =
|
||||
connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.tcp;
|
||||
final isButtonDisabled =
|
||||
isConnecting ||
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
// A running BLE scan must not block TCP connect: connectTcp() stops
|
||||
// any active scan before connecting, so the only reason to disable
|
||||
// the button is a TCP connect already in flight.
|
||||
final isButtonDisabled = isConnecting;
|
||||
return Column(
|
||||
children: [
|
||||
_buildStatusBar(context, connector),
|
||||
_buildTransportLinks(context),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -154,40 +156,32 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (PlatformInfo.supportsUsbSerial)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'tcp_usb_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
heroTag: 'tcp_ble_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransportLinks(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (PlatformInfo.supportsUsbSerial)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -214,7 +208,7 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
statusColor = Colors.orange;
|
||||
} else {
|
||||
statusText = l10n.tcpStatus_notConnected;
|
||||
statusColor = Colors.grey;
|
||||
statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
}
|
||||
|
||||
return Container(
|
||||
@@ -226,15 +220,13 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
Icon(Icons.circle, size: 12, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -274,7 +266,7 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/routing_sheet.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
@@ -118,7 +118,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
if (isAutoRefreshRequest && _isAutoRefreshEnabled) {
|
||||
@@ -178,7 +178,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
_statusTimeout?.cancel();
|
||||
@@ -235,7 +234,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -323,7 +322,11 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
||||
final isFloodMode = widget.contact.pathOverride == -1;
|
||||
final contact = connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||
orElse: () => widget.contact,
|
||||
);
|
||||
final isFloodMode = contact.pathOverride == -1;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -347,70 +350,11 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
IconButton(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(widget.contact, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(widget.contact, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: widget.contact),
|
||||
ContactRoutingSheet.show(context, contact: widget.contact),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
@@ -441,7 +385,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
Center(
|
||||
child: Text(
|
||||
l10n.telemetry_noData,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
if ((_isLoaded || _hasData) &&
|
||||
@@ -718,14 +662,14 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Center(child: Text(l10n.common_disable)),
|
||||
const Positioned(
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -971,21 +915,20 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 130,
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+60
-85
@@ -12,7 +12,6 @@ import '../utils/usb_port_labels.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'scanner_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
|
||||
class UsbScreen extends StatefulWidget {
|
||||
@@ -100,81 +99,62 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
return Column(
|
||||
children: [
|
||||
_buildStatusBar(context, connector),
|
||||
_buildTransportLinks(context),
|
||||
Expanded(child: _buildPortList(context, connector)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isLoading = _isLoadingPorts;
|
||||
final showBle = true;
|
||||
final showTcp = !PlatformInfo.isWeb;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
bottomNavigationBar: _supportsHotPlug
|
||||
? null
|
||||
: SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (showTcp)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'usb_tcp_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
icon: const Icon(Icons.lan),
|
||||
label: Text(context.l10n.connectionChoiceTcpLabel),
|
||||
),
|
||||
if (showTcp && showBle) const SizedBox(width: 12),
|
||||
if (showBle)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ScannerScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
heroTag: 'usb_ble_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
if ((showTcp || showBle) && !_supportsHotPlug)
|
||||
const SizedBox(width: 12),
|
||||
if (!_supportsHotPlug)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: isLoading ? null : _loadPorts,
|
||||
heroTag: 'usb_refresh_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.scanner_scan),
|
||||
),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: _isLoadingPorts ? null : _loadPorts,
|
||||
heroTag: 'usb_refresh_action',
|
||||
icon: _isLoadingPorts
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.scanner_scan),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransportLinks(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (!PlatformInfo.isWeb)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.lan),
|
||||
label: Text(context.l10n.connectionChoiceTcpLabel),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -186,7 +166,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
|
||||
if (_isLoadingPorts) {
|
||||
statusText = l10n.usbStatus_searching;
|
||||
statusColor = Colors.blue;
|
||||
statusColor = Theme.of(context).colorScheme.primary;
|
||||
} else if (connector.isUsbTransportConnected) {
|
||||
switch (connector.state) {
|
||||
case MeshCoreConnectionState.connected:
|
||||
@@ -199,7 +179,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
statusColor = Colors.orange;
|
||||
default:
|
||||
statusText = l10n.usbStatus_notConnected;
|
||||
statusColor = Colors.grey;
|
||||
statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
}
|
||||
} else if (connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.usb) {
|
||||
@@ -207,7 +187,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
statusColor = Colors.orange;
|
||||
} else {
|
||||
statusText = l10n.usbStatus_notConnected;
|
||||
statusColor = Colors.grey;
|
||||
statusColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
}
|
||||
|
||||
return Container(
|
||||
@@ -219,15 +199,13 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
Icon(Icons.circle, size: 12, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -244,11 +222,11 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||
Icon(Icons.usb, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.usbStatus_searching,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -260,12 +238,12 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||
Icon(Icons.usb, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.usbScreenEmptyState,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -294,10 +272,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: showRawName ? Text(rawName) : null,
|
||||
trailing: ElevatedButton(
|
||||
onPressed: isConnecting ? null : () => _connectPort(port),
|
||||
child: Text(l10n.common_connect),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: isConnecting ? null : () => _connectPort(port),
|
||||
);
|
||||
},
|
||||
@@ -387,7 +362,7 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(_friendlyErrorMessage(error)),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -449,6 +449,11 @@ class MessageRetryService extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
void untrack(String messageId) {
|
||||
_timeoutTimers[messageId]?.cancel();
|
||||
_cleanupMessage(messageId);
|
||||
}
|
||||
|
||||
void _cleanupMessage(String messageId) {
|
||||
_moveAckHashesToHistory(messageId);
|
||||
_ackHashToMessageId.removeWhere(
|
||||
|
||||
@@ -248,7 +248,7 @@ class NotificationService {
|
||||
await _notifications.show(
|
||||
id: contactId != null
|
||||
? 'advert:$contactId'.hashCode
|
||||
: DateTime.now().millisecondsSinceEpoch,
|
||||
: DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF,
|
||||
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||
body: contactName,
|
||||
notificationDetails: notificationDetails,
|
||||
@@ -304,7 +304,8 @@ class NotificationService {
|
||||
|
||||
try {
|
||||
await _notifications.show(
|
||||
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||
id: channelIndex?.hashCode ??
|
||||
DateTime.now().millisecondsSinceEpoch & 0x7FFFFFFF,
|
||||
title: channelName,
|
||||
body: body,
|
||||
notificationDetails: notificationDetails,
|
||||
|
||||
@@ -47,6 +47,7 @@ class TranslationService extends ChangeNotifier {
|
||||
_langDetectInit = initLangDetect();
|
||||
}
|
||||
|
||||
bool _disposed = false;
|
||||
bool _isBusy = false;
|
||||
bool _isDownloading = false;
|
||||
bool _cancelDownloadRequested = false;
|
||||
@@ -215,7 +216,7 @@ class TranslationService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
_downloadTotalBytes = totalSize;
|
||||
notifyListeners();
|
||||
_notify();
|
||||
|
||||
DownloadedModelFile downloaded;
|
||||
if (supportsRange &&
|
||||
@@ -268,7 +269,7 @@ class TranslationService extends ChangeNotifier {
|
||||
throw StateError('Model download failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
_downloadTotalBytes ??= response.contentLength;
|
||||
notifyListeners();
|
||||
_notify();
|
||||
final trackedStream = _trackDownloadProgress(response.stream);
|
||||
return await _fileStore.writeModelBytes(
|
||||
fileName: fileName,
|
||||
@@ -313,7 +314,7 @@ class TranslationService extends ChangeNotifier {
|
||||
throw const TranslationDownloadCancelled();
|
||||
}
|
||||
_downloadFileName = 'Merging chunks...';
|
||||
notifyListeners();
|
||||
_notify();
|
||||
combineReached = true;
|
||||
return await _fileStore.combineChunks(
|
||||
fileName: fileName,
|
||||
@@ -361,7 +362,7 @@ class TranslationService extends ChangeNotifier {
|
||||
}
|
||||
_cancelDownloadRequested = true;
|
||||
_lastError = 'Download stopped.';
|
||||
notifyListeners();
|
||||
_notify();
|
||||
}
|
||||
|
||||
Future<void> removeModel(TranslationModelRecord model) async {
|
||||
@@ -469,7 +470,7 @@ class TranslationService extends ChangeNotifier {
|
||||
} catch (error) {
|
||||
_lastError = error.toString();
|
||||
appLogger.warn('Language detection failed: $error');
|
||||
notifyListeners();
|
||||
_notify();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -538,7 +539,7 @@ class TranslationService extends ChangeNotifier {
|
||||
} catch (error) {
|
||||
_lastError = error.toString();
|
||||
appLogger.warn('Translation request failed: $error');
|
||||
notifyListeners();
|
||||
_notify();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -631,6 +632,10 @@ class TranslationService extends ChangeNotifier {
|
||||
final completer = Completer<T>();
|
||||
_setBusy(true);
|
||||
_queue = _queue.then((_) async {
|
||||
if (_disposed) {
|
||||
completer.completeError(StateError('TranslationService disposed.'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
completer.complete(await action());
|
||||
} catch (error, stackTrace) {
|
||||
@@ -648,17 +653,24 @@ class TranslationService extends ChangeNotifier {
|
||||
throw const TranslationDownloadCancelled();
|
||||
}
|
||||
_downloadedBytes += chunk.length;
|
||||
notifyListeners();
|
||||
_notify();
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
|
||||
void _notify() {
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setBusy(bool value) {
|
||||
if (_isBusy == value) {
|
||||
return;
|
||||
}
|
||||
_isBusy = value;
|
||||
notifyListeners();
|
||||
_notify();
|
||||
}
|
||||
|
||||
void _setDownloading(bool value) {
|
||||
@@ -669,11 +681,12 @@ class TranslationService extends ChangeNotifier {
|
||||
_downloadTotalBytes = null;
|
||||
_downloadFileName = null;
|
||||
}
|
||||
notifyListeners();
|
||||
_notify();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
final engine = _engine;
|
||||
_engine = null;
|
||||
_loadedModelPath = null;
|
||||
|
||||
+55
-48
@@ -1,34 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// MeshCore redesign palette — warm field-journal dark theme with
|
||||
/// phosphor-green signal accents. Mirrors values from the redesign spec.
|
||||
/// MeshCore palette — cool slate dark theme with sky-blue accents.
|
||||
class MeshPalette {
|
||||
MeshPalette._();
|
||||
|
||||
// Surfaces (warm near-black, olive undertone)
|
||||
static const bg = Color(0xFF0F1412);
|
||||
static const bg1 = Color(0xFF161C19);
|
||||
static const bg2 = Color(0xFF1D2521);
|
||||
static const bg3 = Color(0xFF28322D);
|
||||
static const bg4 = Color(0xFF34403A);
|
||||
// Surfaces (cool near-black, slate undertone)
|
||||
static const bg = Color(0xFF101417);
|
||||
static const bg1 = Color(0xFF161B1F);
|
||||
static const bg2 = Color(0xFF1D242A);
|
||||
static const bg3 = Color(0xFF28313A);
|
||||
static const bg4 = Color(0xFF344049);
|
||||
|
||||
// Lines
|
||||
static const line = Color(0xFF232C28);
|
||||
static const line2 = Color(0xFF34403A);
|
||||
static const line3 = Color(0xFF48564F);
|
||||
static const line = Color(0xFF222B31);
|
||||
static const line2 = Color(0xFF344049);
|
||||
static const line3 = Color(0xFF485762);
|
||||
|
||||
// Ink
|
||||
static const ink = Color(0xFFEFF3E8);
|
||||
static const ink2 = Color(0xFFBAC4B5);
|
||||
static const ink3 = Color(0xFF7C8B82);
|
||||
static const ink4 = Color(0xFF55635B);
|
||||
static const ink = Color(0xFFE9EEF3);
|
||||
static const ink2 = Color(0xFFB5C0C9);
|
||||
static const ink3 = Color(0xFF7C8A95);
|
||||
static const ink4 = Color(0xFF556470);
|
||||
|
||||
// Signal (phosphor)
|
||||
// Signal-quality green (used only for SNR coloring, not UI chrome)
|
||||
static const signal = Color(0xFF7BEFA8);
|
||||
static const signalDim = Color(0xFF4DC580);
|
||||
static const signalBg = Color(0x177BEFA8); // ~9% alpha
|
||||
static const signalLine = Color(0x427BEFA8); // ~26%
|
||||
static const signalGlow = Color(0x597BEFA8); // ~35%
|
||||
|
||||
// Warn (ember)
|
||||
static const warn = Color(0xFFFFA552);
|
||||
@@ -41,8 +37,9 @@ class MeshPalette {
|
||||
static const alertBg = Color(0x1CFF6A5C);
|
||||
static const alertLine = Color(0x52FF6A5C);
|
||||
|
||||
// Blue (dusk sky)
|
||||
// Blue (sky) — primary accent
|
||||
static const blue = Color(0xFF7FCBF5);
|
||||
static const blueDim = Color(0xFF4A9CC9);
|
||||
static const blueBg = Color(0x1C7FCBF5);
|
||||
static const blueLine = Color(0x477FCBF5);
|
||||
|
||||
@@ -51,20 +48,20 @@ class MeshPalette {
|
||||
static const magentaBg = Color(0x1CDE7FDB);
|
||||
static const magentaLine = Color(0x47DE7FDB);
|
||||
|
||||
// Me bubble (mossy)
|
||||
static const me = Color(0xFF1E3527);
|
||||
static const meBorder = Color(0xFF2D5039);
|
||||
static const meInk = Color(0xFFDEF0DC);
|
||||
// Me bubble (dusk blue)
|
||||
static const me = Color(0xFF1B2C3D);
|
||||
static const meBorder = Color(0xFF2C4A66);
|
||||
static const meInk = Color(0xFFDCE9F5);
|
||||
|
||||
// ── Light variant (used when user explicitly picks light theme)
|
||||
static const lightBg = Color(0xFFF5F3EC);
|
||||
static const lightBg1 = Color(0xFFECE9DF);
|
||||
static const lightBg2 = Color(0xFFE2DED2);
|
||||
static const lightLine = Color(0xFFCAC5B4);
|
||||
static const lightInk = Color(0xFF0F1410);
|
||||
static const lightInk2 = Color(0xFF3D463E);
|
||||
static const lightInk3 = Color(0xFF6A756D);
|
||||
static const lightSignal = Color(0xFF1A7A44);
|
||||
static const lightBg = Color(0xFFF4F6F8);
|
||||
static const lightBg1 = Color(0xFFEAEEF2);
|
||||
static const lightBg2 = Color(0xFFDFE5EA);
|
||||
static const lightLine = Color(0xFFC3CCD4);
|
||||
static const lightInk = Color(0xFF10161B);
|
||||
static const lightInk2 = Color(0xFF3C4853);
|
||||
static const lightInk3 = Color(0xFF69767F);
|
||||
static const lightBlue = Color(0xFF2F6EA8);
|
||||
}
|
||||
|
||||
/// Named font stacks — Flutter falls back to system fonts when the named
|
||||
@@ -115,14 +112,18 @@ class MeshTheme {
|
||||
|
||||
static ThemeData dark() {
|
||||
const scheme = ColorScheme.dark(
|
||||
primary: MeshPalette.signal,
|
||||
onPrimary: Color(0xFF0A1810),
|
||||
primaryContainer: MeshPalette.signalBg,
|
||||
onPrimaryContainer: MeshPalette.signal,
|
||||
secondary: MeshPalette.blue,
|
||||
onSecondary: Color(0xFF0A1520),
|
||||
tertiary: MeshPalette.magenta,
|
||||
onTertiary: Color(0xFF201020),
|
||||
primary: MeshPalette.blue,
|
||||
onPrimary: Color(0xFF0A1A26),
|
||||
primaryContainer: MeshPalette.blueBg,
|
||||
onPrimaryContainer: MeshPalette.blue,
|
||||
secondary: MeshPalette.magenta,
|
||||
onSecondary: Color(0xFF201020),
|
||||
secondaryContainer: Color(0xFF331A33),
|
||||
onSecondaryContainer: MeshPalette.magenta,
|
||||
tertiary: MeshPalette.warn,
|
||||
onTertiary: Color(0xFF1F1206),
|
||||
tertiaryContainer: Color(0xFF3A2710),
|
||||
onTertiaryContainer: Color(0xFFFFC58A),
|
||||
error: MeshPalette.alert,
|
||||
onError: Color(0xFF1A0A08),
|
||||
errorContainer: MeshPalette.alertBg,
|
||||
@@ -141,33 +142,39 @@ class MeshTheme {
|
||||
scrim: Colors.black54,
|
||||
inverseSurface: MeshPalette.ink,
|
||||
onInverseSurface: MeshPalette.bg,
|
||||
inversePrimary: MeshPalette.signalDim,
|
||||
inversePrimary: MeshPalette.blueDim,
|
||||
);
|
||||
return _build(scheme, Brightness.dark);
|
||||
}
|
||||
|
||||
static ThemeData light() {
|
||||
const scheme = ColorScheme.light(
|
||||
primary: MeshPalette.lightSignal,
|
||||
primary: MeshPalette.lightBlue,
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: Color(0xFFD4E8D8),
|
||||
onPrimaryContainer: MeshPalette.lightSignal,
|
||||
secondary: Color(0xFF2F6EA8),
|
||||
primaryContainer: Color(0xFFD3E4F5),
|
||||
onPrimaryContainer: Color(0xFF12354F),
|
||||
secondary: Color(0xFF8C4A8A),
|
||||
onSecondary: Colors.white,
|
||||
tertiary: Color(0xFF8C4A8A),
|
||||
secondaryContainer: Color(0xFFEFD6EE),
|
||||
onSecondaryContainer: Color(0xFF3D1A3C),
|
||||
tertiary: Color(0xFF9A5B16),
|
||||
onTertiary: Colors.white,
|
||||
tertiaryContainer: Color(0xFFF8E3C9),
|
||||
onTertiaryContainer: Color(0xFF4A2A05),
|
||||
error: Color(0xFFB53D2F),
|
||||
onError: Colors.white,
|
||||
errorContainer: Color(0xFFF6D9D4),
|
||||
onErrorContainer: Color(0xFF5C1A12),
|
||||
surface: MeshPalette.lightBg,
|
||||
onSurface: MeshPalette.lightInk,
|
||||
surfaceContainerLowest: MeshPalette.lightBg,
|
||||
surfaceContainerLow: MeshPalette.lightBg1,
|
||||
surfaceContainer: MeshPalette.lightBg1,
|
||||
surfaceContainerHigh: MeshPalette.lightBg2,
|
||||
surfaceContainerHighest: Color(0xFFD5D0C0),
|
||||
surfaceContainerHighest: Color(0xFFD2DAE1),
|
||||
onSurfaceVariant: MeshPalette.lightInk2,
|
||||
outline: MeshPalette.lightLine,
|
||||
outlineVariant: Color(0xFFDBD6C6),
|
||||
outlineVariant: Color(0xFFD8DEE5),
|
||||
);
|
||||
return _build(scheme, Brightness.light);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class ByteCountedTextField extends StatelessWidget {
|
||||
final counterColor = ratio > errorThreshold
|
||||
? Theme.of(context).colorScheme.error
|
||||
: ratio > warningThreshold
|
||||
? Colors.orange
|
||||
? Theme.of(context).colorScheme.tertiary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
|
||||
return Column(
|
||||
@@ -118,8 +118,9 @@ class ByteCountedTextField extends StatelessWidget {
|
||||
textInputAction: textInputAction,
|
||||
onSubmitted: onSubmitted,
|
||||
),
|
||||
if (showCounter)
|
||||
Padding(
|
||||
Opacity(
|
||||
opacity: showCounter ? 1 : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, right: 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -129,6 +130,7 @@ class ByteCountedTextField extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,9 +6,15 @@ import 'signal_ui.dart';
|
||||
/// A reusable tile widget for displaying a MeshCore device in a list
|
||||
class DeviceTile extends StatelessWidget {
|
||||
final ScanResult scanResult;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onTap;
|
||||
final bool isConnecting;
|
||||
|
||||
const DeviceTile({super.key, required this.scanResult, required this.onTap});
|
||||
const DeviceTile({
|
||||
super.key,
|
||||
required this.scanResult,
|
||||
required this.onTap,
|
||||
this.isConnecting = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -19,16 +25,20 @@ class DeviceTile extends StatelessWidget {
|
||||
: scanResult.advertisementData.advName;
|
||||
|
||||
return ListTile(
|
||||
enabled: onTap != null || isConnecting,
|
||||
leading: _buildSignalIcon(rssi),
|
||||
title: Text(
|
||||
name.isNotEmpty ? name : context.l10n.common_unknownDevice,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(device.remoteId.toString()),
|
||||
trailing: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
child: Text(context.l10n.common_connect),
|
||||
),
|
||||
trailing: isConnecting
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: null,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,18 +17,22 @@ class EmptyState extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final onSurfaceVariant = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 64, color: Colors.grey[400]),
|
||||
Icon(icon, size: 64, color: onSurfaceVariant.withValues(alpha: 0.6)),
|
||||
const SizedBox(height: 16),
|
||||
Text(title, style: TextStyle(fontSize: 16, color: Colors.grey[600])),
|
||||
Text(title, style: TextStyle(fontSize: 16, color: onSurfaceVariant)),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: onSurfaceVariant.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -180,7 +180,7 @@ class _GifPickerState extends State<GifPicker> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.gifPicker_poweredBy,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -197,11 +197,11 @@ class _GifPickerState extends State<GifPicker> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
|
||||
Icon(Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_error!,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
@@ -219,11 +219,11 @@ class _GifPickerState extends State<GifPicker> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
|
||||
Icon(Icons.search_off, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.gifPicker_noGifsFound,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,36 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class MessageStatusIcon extends StatelessWidget {
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class MessageStatusIcon extends StatefulWidget {
|
||||
final bool isAcked;
|
||||
final bool isFailed;
|
||||
final bool isPending;
|
||||
final bool isRepeated;
|
||||
final double size;
|
||||
|
||||
/// Base tint for the sent/sending state. On a colored (outgoing) bubble a
|
||||
/// plain grey tick is nearly invisible, so callers can pass the bubble's own
|
||||
/// meta/text color for contrast. Falls back to [ColorScheme.onSurfaceVariant].
|
||||
final Color? onColor;
|
||||
|
||||
const MessageStatusIcon({
|
||||
super.key,
|
||||
required this.isAcked,
|
||||
this.isFailed = false,
|
||||
this.isPending = false,
|
||||
this.isRepeated = false,
|
||||
this.size = 14,
|
||||
this.onColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MessageStatusIcon> createState() => _MessageStatusIconState();
|
||||
}
|
||||
|
||||
class _MessageStatusIconState extends State<MessageStatusIcon>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
);
|
||||
if (widget.isPending) _controller.repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageStatusIcon oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.isPending && !_controller.isAnimating) {
|
||||
_controller.repeat();
|
||||
} else if (!widget.isPending && _controller.isAnimating) {
|
||||
_controller
|
||||
..stop()
|
||||
..reset();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final double size = widget.size;
|
||||
final Color baseColor = widget.onColor ?? colorScheme.onSurfaceVariant;
|
||||
|
||||
if (widget.isFailed) {
|
||||
return Semantics(
|
||||
label: l10n.messageStatus_failed,
|
||||
child: Icon(Icons.cancel, size: size, color: colorScheme.error),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.isPending) {
|
||||
return Semantics(
|
||||
label: l10n.messageStatus_pending,
|
||||
child: _SendingDots(
|
||||
controller: _controller,
|
||||
color: baseColor,
|
||||
size: size,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final bool delivered = widget.isAcked || widget.isRepeated;
|
||||
final String label = widget.isRepeated
|
||||
? l10n.messageStatus_repeated
|
||||
: widget.isAcked
|
||||
? l10n.messageStatus_delivered
|
||||
: l10n.messageStatus_sent;
|
||||
final Color color = delivered ? colorScheme.tertiary : baseColor;
|
||||
|
||||
return Semantics(
|
||||
label: label,
|
||||
child: delivered
|
||||
? SvgPicture.asset(
|
||||
'assets/icons/done_all.svg',
|
||||
width: size,
|
||||
height: size,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
)
|
||||
: Icon(Icons.done, size: size, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Three dots that pulse left-to-right while a message is in flight.
|
||||
class _SendingDots extends StatelessWidget {
|
||||
final AnimationController controller;
|
||||
final Color color;
|
||||
final double size;
|
||||
|
||||
const _SendingDots({
|
||||
required this.controller,
|
||||
required this.color,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isFailed) {
|
||||
return Icon(Icons.cancel, size: size, color: Colors.red);
|
||||
}
|
||||
|
||||
final Color color;
|
||||
if (isAcked) {
|
||||
color = Colors.green;
|
||||
} else {
|
||||
color = Colors.grey;
|
||||
}
|
||||
|
||||
return SvgPicture.asset(
|
||||
'assets/icons/done_all.svg',
|
||||
width: size,
|
||||
final double dot = (size * 0.24).clamp(2.0, 4.0);
|
||||
return SizedBox(
|
||||
height: size,
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
child: AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: List.generate(3, (i) {
|
||||
final double phase = (controller.value - i * 0.18) % 1.0;
|
||||
final double t = phase < 0.5 ? phase * 2 : (1 - phase) * 2;
|
||||
final double opacity = 0.25 + 0.75 * t.clamp(0.0, 1.0);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: dot * 0.28),
|
||||
child: Container(
|
||||
width: dot,
|
||||
height: dot,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: opacity),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,12 @@ class QuickSwitchBar extends StatelessWidget {
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: _buildIconWithBadge(
|
||||
context,
|
||||
const Icon(Icons.people_outline),
|
||||
contactsUnreadCount,
|
||||
),
|
||||
selectedIcon: _buildIconWithBadge(
|
||||
context,
|
||||
const Icon(Icons.people),
|
||||
contactsUnreadCount,
|
||||
),
|
||||
@@ -78,10 +80,12 @@ class QuickSwitchBar extends StatelessWidget {
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: _buildIconWithBadge(
|
||||
context,
|
||||
const Icon(Icons.tag),
|
||||
channelsUnreadCount,
|
||||
),
|
||||
selectedIcon: _buildIconWithBadge(
|
||||
context,
|
||||
const Icon(Icons.tag),
|
||||
channelsUnreadCount,
|
||||
),
|
||||
@@ -101,26 +105,9 @@ class QuickSwitchBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconWithBadge(Icon icon, int count) {
|
||||
Widget _buildIconWithBadge(BuildContext context, Icon icon, int count) {
|
||||
if (count <= 0) return icon;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -2,
|
||||
top: -2,
|
||||
child: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.redAccent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
final label = count > 99 ? '99+' : '$count';
|
||||
return Badge(label: Text(label), child: icon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +59,15 @@ class _RadioStatsIconButtonState extends State<RadioStatsIconButton> {
|
||||
active: connector.radioStatsAirActivityPulse,
|
||||
);
|
||||
if (widget.compact) {
|
||||
return GestureDetector(
|
||||
onTap: () => pushCompanionRadioStatsScreen(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: dot,
|
||||
return Semantics(
|
||||
label: context.l10n.radioStats_tooltip,
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => pushCompanionRadioStatsScreen(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: dot,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import '../utils/platform_info.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -11,7 +10,7 @@ import '../services/storage_service.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'path_management_dialog.dart';
|
||||
import 'routing_sheet.dart';
|
||||
|
||||
class RepeaterLoginDialog extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -276,7 +275,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.cell_tower, color: Colors.orange),
|
||||
Icon(Icons.cell_tower, color: Theme.of(context).colorScheme.tertiary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -288,7 +287,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -365,9 +364,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) => _handleLogin(),
|
||||
autofocus:
|
||||
!PlatformInfo.isMobile &&
|
||||
_passwordController.text.isEmpty,
|
||||
autofocus: _passwordController.text.isEmpty,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
@@ -469,14 +466,14 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
repeater.pathLabel(context.l10n),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
ContactRoutingSheet.show(context, contact: repeater),
|
||||
icon: const Icon(Icons.timeline, size: 18),
|
||||
label: Text(l10n.login_managePaths),
|
||||
),
|
||||
@@ -497,12 +494,12 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
@@ -12,7 +12,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'path_management_dialog.dart';
|
||||
import 'routing_sheet.dart';
|
||||
|
||||
class RoomLoginDialog extends StatefulWidget {
|
||||
final Contact room;
|
||||
@@ -181,7 +181,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.login_failed(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -232,7 +232,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.group, color: Colors.purple),
|
||||
Icon(Icons.group, color: Theme.of(context).colorScheme.secondary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -244,7 +244,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -395,14 +395,14 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
repeater.pathLabel(context.l10n),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
ContactRoutingSheet.show(context, contact: repeater),
|
||||
icon: const Icon(Icons.timeline, size: 18),
|
||||
label: Text(l10n.login_managePaths),
|
||||
),
|
||||
@@ -423,12 +423,12 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -147,19 +147,19 @@ class _TelemetryLocationMapState extends State<TelemetryLocationMap> {
|
||||
children: [
|
||||
_MapButton(
|
||||
icon: Icons.add,
|
||||
tooltip: 'Zoom in',
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomBy(1),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_MapButton(
|
||||
icon: Icons.remove,
|
||||
tooltip: 'Zoom out',
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomBy(-1),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_MapButton(
|
||||
icon: Icons.my_location,
|
||||
tooltip: 'Center map',
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: () =>
|
||||
_mapController.move(_position, _initialZoom),
|
||||
),
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ dependencies:
|
||||
path_provider: ^2.1.5
|
||||
share_plus: ^12.0.1
|
||||
build_pipe: ^0.3.1
|
||||
material_symbols_icons: ^4.2906.0
|
||||
material_symbols_icons: ^4.2928.1
|
||||
web: ^1.1.1
|
||||
flutter_svg: ^2.0.10+1
|
||||
flutter_blue_plus_platform_interface: ^8.2.1
|
||||
|
||||
@@ -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())
|
||||
@@ -123,11 +123,13 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.widgetWithText(FloatingActionButton, 'TCP'));
|
||||
final scannerContext = tester.element(find.byType(ScannerScreen));
|
||||
final scannerL10n = AppLocalizations.of(scannerContext);
|
||||
await tester.tap(find.byTooltip(scannerL10n.connectionChoiceTcpLabel));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TcpScreen), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(FloatingActionButton, 'Bluetooth'));
|
||||
await tester.tap(find.widgetWithText(OutlinedButton, 'Bluetooth'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(TcpScreen), findsNothing);
|
||||
|
||||
@@ -157,10 +157,18 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final context = tester.element(find.byType(ScannerScreen));
|
||||
final l10n = AppLocalizations.of(context);
|
||||
if (PlatformInfo.supportsUsbSerial) {
|
||||
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget);
|
||||
expect(
|
||||
find.byTooltip(l10n.connectionChoiceUsbLabel),
|
||||
findsOneWidget,
|
||||
);
|
||||
} else {
|
||||
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing);
|
||||
expect(
|
||||
find.byTooltip(l10n.connectionChoiceUsbLabel),
|
||||
findsNothing,
|
||||
);
|
||||
}
|
||||
|
||||
// ScannerScreen.dispose() schedules disconnect work that debounces notify.
|
||||
@@ -186,13 +194,13 @@ void main() {
|
||||
|
||||
final context = tester.element(find.byType(ScannerScreen));
|
||||
final l10n = AppLocalizations.of(context);
|
||||
expect(find.text(l10n.scanner_scan), findsOneWidget);
|
||||
expect(find.text(l10n.scanner_scan), findsWidgets);
|
||||
|
||||
if (PlatformInfo.supportsUsbSerial) {
|
||||
expect(find.text(l10n.connectionChoiceUsbLabel), findsOneWidget);
|
||||
expect(find.byTooltip(l10n.connectionChoiceUsbLabel), findsOneWidget);
|
||||
}
|
||||
if (!PlatformInfo.isWeb) {
|
||||
expect(find.text(l10n.connectionChoiceTcpLabel), findsOneWidget);
|
||||
expect(find.byTooltip(l10n.connectionChoiceTcpLabel), findsOneWidget);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
|
||||
+987
-1
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user