mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-05 23:42:57 +10:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00636c9084 | |||
| 67238468ce | |||
| ae32e76563 | |||
| 5572c9ee75 | |||
| f1d93bd5e8 | |||
| eb597b6c68 | |||
| efe21c4e87 | |||
| 38fece3313 | |||
| 3af3cce606 | |||
| 026ec6f7de | |||
| eb50249b93 | |||
| 99c0ab7e22 | |||
| 2950a9a687 | |||
| 1b3de54873 | |||
| 20a9ef3c2b | |||
| a741e12ad1 | |||
| e54f30d6fb | |||
| e1d23ad2c7 | |||
| f07993b367 | |||
| 0e5f1a45c4 | |||
| f10aeaeba8 | |||
| 00e4f52d75 | |||
| 3ea2e4763e | |||
| 94d9afe8b1 | |||
| 7db3a12723 | |||
| fcf10b4a73 | |||
| 7f353490cf | |||
| 46683e0ec2 | |||
| 4e368d562d | |||
| 92d3009eb4 | |||
| f8d00caae0 | |||
| e03d80b71f | |||
| b7d0db8d1c | |||
| 6ae3f612ae | |||
| 40d3941aab | |||
| e53c493e78 | |||
| 54e0dae172 | |||
| 066aba7c5d | |||
| 5e446207c6 | |||
| 820bac0db0 | |||
| 6b6a881c7a | |||
| 8ef8a38495 | |||
| ddcda4ba5a | |||
| b572314ae9 | |||
| e97fb9bd24 | |||
| a4bbeffddc | |||
| 37ec8f2f05 | |||
| 39cd6d5514 | |||
| 44eb4fad58 | |||
| 1a209cbcfc | |||
| 33a8f34463 | |||
| ce8e8f0d5b | |||
| aa2d0f1927 | |||
| 0757c8e53a | |||
| add4731d05 | |||
| 7dc162d968 | |||
| 8ba4bbfbc5 | |||
| cac6abfef1 | |||
| 5354acb1d3 | |||
| fae416fb34 | |||
| 69433b6d89 | |||
| ea3b9609fc | |||
| 20a9939314 | |||
| c7b7deb0f6 | |||
| 82e04e8090 | |||
| f299608296 | |||
| 7dcec5b4ee | |||
| e4684b585a | |||
| 8386f262e1 | |||
| 45cd8a56a3 | |||
| 754f8a6c62 | |||
| c4f54efd77 | |||
| 637e08d22c | |||
| 32dc0fca22 | |||
| b5aa294fc1 | |||
| 26516baf67 | |||
| 4879b136f8 | |||
| bdd7fc0cdd | |||
| 5ea044af10 | |||
| 9d20be1c06 | |||
| 9436c2d45a | |||
| 17e55e96bb | |||
| e4cfbb57b4 | |||
| d9f9ff58b4 | |||
| a059f1be45 | |||
| 9e46f8b44c | |||
| a934781009 | |||
| 5fe6738f25 | |||
| c1bcf261d7 | |||
| b570539a2d | |||
| 89a14c2719 | |||
| 4ad01ed43c | |||
| ffaa4033ae | |||
| 1a4fd1b477 | |||
| e1555ce380 | |||
| c7933d363b | |||
| 08ffb978cf | |||
| c5ec60638c | |||
| 75ec3b6116 | |||
| 45c9823c6f | |||
| 45658a7612 | |||
| a14833494e | |||
| 457b44de3a | |||
| 36d4a10396 | |||
| 77566b0fe1 | |||
| 7633327f45 | |||
| 6b4b2d7ce6 | |||
| 10b63e0df2 | |||
| ba6d751346 | |||
| 96d222a580 | |||
| 01ad8471cc | |||
| 2b826757cb | |||
| 9bf649e2c6 | |||
| c7a2bf9a95 | |||
| 82adbd761b | |||
| 9a8bdf00dc | |||
| 8b30342113 | |||
| 817c60a155 | |||
| f08e86cf97 | |||
| a6bb9490a1 | |||
| e4e8bfa4ef | |||
| d1e45fc2ba | |||
| 32fa96431e | |||
| 1e9508d401 | |||
| 36697c6e61 | |||
| c9145c99d3 | |||
| 6b6d9caeeb | |||
| d0e3767db6 | |||
| f9cb0c80a5 | |||
| a26d14bd46 | |||
| 0ef2194fb0 | |||
| 3664ae34cd |
@@ -33,6 +33,9 @@ migrate_working_dir/
|
||||
pubspec.lock
|
||||
/build/
|
||||
/coverage/
|
||||
# fvm project files
|
||||
.fvm/
|
||||
.fvmrc
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
@@ -84,6 +87,7 @@ keystore.properties
|
||||
# IDE
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
.contextstream/
|
||||
|
||||
# Cloudflare Wrangler
|
||||
.wrangler
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## BLE Frames & Protocol Notes
|
||||
- Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`.
|
||||
- Discovery: scans for device name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`.
|
||||
- Discovery: scans for device names matching known prefixes and filters by `platformName`/`advertisementData.advName`.
|
||||
- Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`.
|
||||
- Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57.
|
||||
- Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25.
|
||||
|
||||
@@ -61,7 +61,7 @@ lib/
|
||||
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
|
||||
|
||||
### Device Discovery
|
||||
- Scans for devices with name prefix `MeshCore-`
|
||||
- Scans for devices with known name prefixes
|
||||
- Filters by `platformName` or `advertisementData.advName`
|
||||
|
||||
### Connection States
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# How to contribute to Meshcore Open
|
||||
|
||||
Before submitting any pull requests (PR), please review the following information.
|
||||
|
||||
Unsolicited PRs without previous discussion or open issues may be
|
||||
rejected. As may changes that are too broad (i.e. 100 files changed) or that
|
||||
cover too many separate changes. If the changes are clearly AI generated they
|
||||
may also be rejected. [See more](#ai-use)
|
||||
|
||||
## First Step Checklist
|
||||
|
||||
### **Did you find a bug?**
|
||||
|
||||
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/zjs81/meshcore-open/issues).
|
||||
|
||||
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/zjs81/meshcore-open/issues/new).
|
||||
Be sure to include a **title and clear description**, as much relevant
|
||||
information as possible, and a **code sample** or an **executable test case**
|
||||
demonstrating the expected behavior that is not occurring. You can also include
|
||||
screenshots or video.
|
||||
|
||||
* DO NOT start work and submit a PR at this time, please discuss the issue and
|
||||
your implementation plan first.
|
||||
|
||||
### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
|
||||
|
||||
Changes that are cosmetic in nature and do not add anything substantial to the
|
||||
stability, functionality, or testability of the application will generally not
|
||||
be accepted.
|
||||
|
||||
### **Do you intend to add a new feature or change an existing one?**
|
||||
|
||||
* Suggest your change in a new issue as a feature request.
|
||||
|
||||
* DO NOT start work and submit a PR at this time, please discuss the change and
|
||||
your implementation plan first.
|
||||
|
||||
* After it is generally decided that the feature or change fits the goals of the
|
||||
project you can start work or open a PR if you have already started.
|
||||
|
||||
## Submitting your patch
|
||||
|
||||
* All changes should be based on the `dev` branch. When creating your PR please
|
||||
be sure to change the target to merge into dev, and when starting work on a new
|
||||
branch be sure to start on latest `dev`.
|
||||
|
||||
* Ensure the PR description clearly describes the problem and solution. Include
|
||||
the relevant issue number if applicable.
|
||||
|
||||
* The PR should contain **one commit** only, the commit message should have a
|
||||
clear title followed by a new line and then brief description if needed. PR with
|
||||
multiple commits will be squashed into one before merging if required. See
|
||||
[Git Mastery](https://git-mastery.org/lessons/commitMessage/) for more
|
||||
information on good commit messages.
|
||||
|
||||
* **Before committing changes** on your branch, be sure to run both
|
||||
`dart format .` and `flutter analyze`. The continuous development checks will
|
||||
fail if issues here are not addressed before hand.
|
||||
|
||||
## AI-use
|
||||
|
||||
Everyone loves some help, AI agents are a tool in many of our belts. The project
|
||||
is not anti-AI.
|
||||
|
||||
There are some limits to acceptable use however. Generally:
|
||||
|
||||
* All code generated by AI should be thoroughly reviewed by the contributor.
|
||||
* The changes should be tightly controlled to not change anything out of scope
|
||||
for the patch, bug fix, etc.
|
||||
* The contributor should have a good understanding of what the code does and how
|
||||
the application works in order to effectively be able to manage the agent.
|
||||
@@ -150,7 +150,8 @@ lib/
|
||||
├── main.dart # App entry point
|
||||
├── connector/
|
||||
│ ├── meshcore_connector.dart # BLE communication & state management
|
||||
│ └── meshcore_protocol.dart # Protocol definitions & frame parsing
|
||||
│ ├── meshcore_protocol.dart # Protocol definitions & frame parsing
|
||||
│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!)
|
||||
├── screens/
|
||||
│ ├── scanner_screen.dart # Device scanning (home screen)
|
||||
│ ├── contacts_screen.dart # Contact list
|
||||
@@ -184,7 +185,15 @@ lib/
|
||||
|
||||
### Device Discovery
|
||||
|
||||
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
|
||||
Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently:
|
||||
- `MeshCore-`
|
||||
- `Whisper-`
|
||||
- `WisCore-`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
|
||||
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
|
||||
|
||||
|
||||
### Message Format
|
||||
|
||||
|
||||
@@ -21,7 +21,12 @@ The MeshCore BLE protocol implements a binary frame-based communication system u
|
||||
|
||||
### Connection Flow
|
||||
|
||||
1. **Scan** for devices with name prefix `MeshCore-`
|
||||
1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||
- `MeshCore-`
|
||||
- `Whisper-`
|
||||
- `WisCore-`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
2. **Connect** with 15-second timeout
|
||||
3. **Request MTU** of 185 bytes (falls back to default if unsupported)
|
||||
4. **Discover services** and locate NUS characteristics
|
||||
|
||||
@@ -49,7 +49,12 @@ enum MeshCoreConnectionState {
|
||||
|
||||
## BLE Connection Lifecycle
|
||||
|
||||
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
|
||||
1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||
- `MeshCore-`
|
||||
- `Whisper-`
|
||||
- `WisCore-`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
2. **Connect** with 15-second timeout
|
||||
3. **Request MTU** 185 bytes (non-web only)
|
||||
4. **Discover services** and locate NUS
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../models/companion_radio_stats.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
@@ -26,6 +27,7 @@ import '../services/path_history_service.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/background_service.dart';
|
||||
import '../services/timeout_prediction_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import 'meshcore_connector_usb.dart';
|
||||
import 'meshcore_connector_tcp.dart';
|
||||
@@ -102,6 +104,22 @@ class RepeaterBatterySnapshot {
|
||||
});
|
||||
}
|
||||
|
||||
class MeshCoreRadioStateSnapshot {
|
||||
final int freqHz;
|
||||
final int bwHz;
|
||||
final int sf;
|
||||
final int cr;
|
||||
final int txPowerDbm;
|
||||
|
||||
const MeshCoreRadioStateSnapshot({
|
||||
required this.freqHz,
|
||||
required this.bwHz,
|
||||
required this.sf,
|
||||
required this.cr,
|
||||
required this.txPowerDbm,
|
||||
});
|
||||
}
|
||||
|
||||
class MeshCoreConnector extends ChangeNotifier {
|
||||
// Message windowing to limit memory usage
|
||||
static const int _messageWindowSize = 200;
|
||||
@@ -167,6 +185,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int? _currentSf;
|
||||
int? _currentCr;
|
||||
bool? _clientRepeat;
|
||||
MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState;
|
||||
int? _firmwareVerCode;
|
||||
int _pathHashByteWidth = 1;
|
||||
CompanionRadioStats? _latestRadioStats;
|
||||
@@ -180,6 +199,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _isLoadingChannels = false;
|
||||
bool _hasLoadedChannels = false;
|
||||
TimeoutPredictionService? _timeoutPredictionService;
|
||||
TranslationService? _translationService;
|
||||
// Intentionally global (not per-contact): tracks overall network activity.
|
||||
// Frequent RX from any source indicates a busy network with more collisions.
|
||||
DateTime _lastRxTime = DateTime.now();
|
||||
@@ -193,6 +213,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
static const int _contactMsgBackoffFallbackMs = 5000;
|
||||
static const int _contactMsgBackoffMinMs = 500;
|
||||
static const int _contactMsgBackoffMaxMs = 15000;
|
||||
int _pollingInterval = 30;
|
||||
bool _batteryRequested = false;
|
||||
bool _awaitingSelfInfo = false;
|
||||
bool _hasReceivedDeviceInfo = false;
|
||||
@@ -323,8 +344,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
List<Contact> get allContacts => List.unmodifiable([
|
||||
..._contacts,
|
||||
..._discoveredContacts.where((c) => !c.isActive),
|
||||
..._discoveredContacts.where(
|
||||
(c) => !c.isActive && c.publicKeyHex != selfPublicKeyHex,
|
||||
),
|
||||
]);
|
||||
|
||||
List<Contact> get allContactsUnfiltered =>
|
||||
List.unmodifiable([..._contacts, ..._discoveredContacts]);
|
||||
|
||||
List<Contact> get discoveredContacts {
|
||||
return List.unmodifiable(_discoveredContacts);
|
||||
}
|
||||
@@ -359,6 +386,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int? get currentBwHz => _currentBwHz;
|
||||
int? get currentSf => _currentSf;
|
||||
int? get currentCr => _currentCr;
|
||||
MeshCoreRadioStateSnapshot? get rememberedNonRepeatRadioState =>
|
||||
_rememberedNonRepeatRadioState;
|
||||
bool? get autoAddUsers => _autoAddUsers;
|
||||
bool? get autoAddRepeaters => _autoAddRepeaters;
|
||||
bool? get autoAddRoomServers => _autoAddRoomServers;
|
||||
@@ -370,6 +399,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int get advertLocationPolicy => _advertLocPolicy;
|
||||
int get multiAcks => _multiAcks;
|
||||
bool? get clientRepeat => _clientRepeat;
|
||||
void rememberNonRepeatRadioState(MeshCoreRadioStateSnapshot snapshot) {
|
||||
_rememberedNonRepeatRadioState = snapshot;
|
||||
}
|
||||
|
||||
int? get firmwareVerCode => _firmwareVerCode;
|
||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||
int? get batteryMillivolts => _batteryMillivolts;
|
||||
@@ -482,7 +515,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
String _messageMergeKey(Message message) {
|
||||
final messageId = message.messageId;
|
||||
if (messageId != null && messageId.isNotEmpty) {
|
||||
if (messageId.isNotEmpty) {
|
||||
return 'id:$messageId';
|
||||
}
|
||||
return 'fallback:${message.senderKeyHex}:${message.isOutgoing}:${message.isCli}:${message.timestamp.millisecondsSinceEpoch}:${message.text}';
|
||||
@@ -625,6 +658,27 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void setContactUnreadCount(String contactKeyHex, int count) {
|
||||
_contactUnreadCount[contactKeyHex] = count;
|
||||
_unreadStore.saveContactUnreadCount(
|
||||
Map<String, int>.from(_contactUnreadCount),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setChannelUnreadCount(int channelIndex, int count) {
|
||||
final channel = _findChannelByIndex(channelIndex);
|
||||
if (channel != null) {
|
||||
channel.unreadCount = count;
|
||||
unawaited(
|
||||
_channelStore.saveChannels(
|
||||
_channels.isNotEmpty ? _channels : _cachedChannels,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void markChannelRead(int channelIndex) {
|
||||
final channel = _findChannelByIndex(channelIndex);
|
||||
if (channel != null && channel.unreadCount > 0) {
|
||||
@@ -721,6 +775,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
required MessageRetryService retryService,
|
||||
required PathHistoryService pathHistoryService,
|
||||
AppSettingsService? appSettingsService,
|
||||
TranslationService? translationService,
|
||||
BleDebugLogService? bleDebugLogService,
|
||||
AppDebugLogService? appDebugLogService,
|
||||
BackgroundService? backgroundService,
|
||||
@@ -729,6 +784,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_retryService = retryService;
|
||||
_pathHistoryService = pathHistoryService;
|
||||
_appSettingsService = appSettingsService;
|
||||
_translationService = translationService;
|
||||
_bleDebugLogService = bleDebugLogService;
|
||||
_appDebugLogService = appDebugLogService;
|
||||
_backgroundService = backgroundService;
|
||||
@@ -952,6 +1008,126 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _translateIncomingContactMessage(
|
||||
String contactKeyHex,
|
||||
Message message,
|
||||
) async {
|
||||
try {
|
||||
final service = _translationService;
|
||||
if (service == null ||
|
||||
!service.shouldTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: message.isCli,
|
||||
isOutgoing: message.isOutgoing,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
final targetLanguageCode = service.resolvedIncomingLanguageCode(
|
||||
_appSettingsService?.settings.languageOverride,
|
||||
);
|
||||
final result = await service.translateIncomingText(
|
||||
text: message.text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
final translated = result.status == MessageTranslationStatus.completed
|
||||
? result.translatedText
|
||||
: null;
|
||||
_updateStoredContactMessage(
|
||||
contactKeyHex,
|
||||
message.messageId,
|
||||
(current) => current.copyWith(
|
||||
translatedText: translated,
|
||||
translatedLanguageCode: result.detectedLanguageCode,
|
||||
translationStatus: result.status,
|
||||
translationModelId: result.modelId,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
appLogger.warn('Translation failed for contact message: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _translateIncomingChannelMessage(
|
||||
int channelIndex,
|
||||
ChannelMessage message,
|
||||
) async {
|
||||
try {
|
||||
final service = _translationService;
|
||||
if (service == null ||
|
||||
!service.shouldTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: false,
|
||||
isOutgoing: message.isOutgoing,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
final targetLanguageCode = service.resolvedIncomingLanguageCode(
|
||||
_appSettingsService?.settings.languageOverride,
|
||||
);
|
||||
final result = await service.translateIncomingText(
|
||||
text: message.text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
final translated = result.status == MessageTranslationStatus.completed
|
||||
? result.translatedText
|
||||
: null;
|
||||
_updateStoredChannelMessage(
|
||||
channelIndex,
|
||||
message.messageId,
|
||||
(current) => current.copyWith(
|
||||
translatedText: translated,
|
||||
translatedLanguageCode: result.detectedLanguageCode,
|
||||
translationStatus: result.status,
|
||||
translationModelId: result.modelId,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
appLogger.warn('Translation failed for channel message: $error');
|
||||
}
|
||||
}
|
||||
|
||||
void _updateStoredContactMessage(
|
||||
String contactKeyHex,
|
||||
String messageId,
|
||||
Message Function(Message current) update,
|
||||
) {
|
||||
final messages = _conversations[contactKeyHex];
|
||||
if (messages == null) {
|
||||
return;
|
||||
}
|
||||
final index = messages.indexWhere((entry) => entry.messageId == messageId);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
messages[index] = update(messages[index]);
|
||||
_messageStore.saveMessages(contactKeyHex, messages);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _updateStoredChannelMessage(
|
||||
int channelIndex,
|
||||
String messageId,
|
||||
ChannelMessage Function(ChannelMessage current) update,
|
||||
) {
|
||||
final messages = _channelMessages[channelIndex];
|
||||
if (messages == null) {
|
||||
return;
|
||||
}
|
||||
final index = messages.indexWhere((entry) => entry.messageId == messageId);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
messages[index] = update(messages[index]);
|
||||
_channelMessageStore.saveChannelMessages(channelIndex, messages);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _recordPathResult(
|
||||
String contactPubKeyHex,
|
||||
PathSelection selection,
|
||||
@@ -1985,6 +2161,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_bleInitialSyncStarted = true;
|
||||
_pendingInitialContactsSync = true;
|
||||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
@@ -2116,6 +2293,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_channelSyncTimeout?.cancel();
|
||||
_channelSyncTimeout = null;
|
||||
_channelSyncRetries = 0;
|
||||
await _translationService?.releaseModel();
|
||||
|
||||
if (!skipBleDeviceDisconnect) {
|
||||
try {
|
||||
@@ -2145,6 +2323,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_selfLatitude = null;
|
||||
_selfLongitude = null;
|
||||
_clientRepeat = null;
|
||||
_rememberedNonRepeatRadioState = null;
|
||||
_firmwareVerCode = null;
|
||||
_batteryMillivolts = null;
|
||||
_repeaterBatterySnapshots.clear();
|
||||
@@ -2242,9 +2421,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_batteryPollTimer = null;
|
||||
}
|
||||
|
||||
void setPollingInterval(int i) {
|
||||
_pollingInterval = i.clamp(1, 60);
|
||||
if (isConnected) {
|
||||
_startRadioStatsPolling();
|
||||
}
|
||||
}
|
||||
|
||||
void _startRadioStatsPolling() {
|
||||
_radioStatsPollTimer?.cancel();
|
||||
_radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
_radioStatsPollTimer = Timer.periodic(Duration(seconds: _pollingInterval), (
|
||||
_,
|
||||
) {
|
||||
if (!isConnected) {
|
||||
_stopRadioStatsPolling();
|
||||
return;
|
||||
@@ -2369,6 +2557,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
Contact getFromDiscovered(Contact contact) {
|
||||
final tmp = _discoveredContacts.firstWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
orElse: () => contact,
|
||||
);
|
||||
return contact.copyWith(
|
||||
rawPacket: tmp.rawPacket,
|
||||
latitude: tmp.latitude,
|
||||
longitude: tmp.longitude,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getContacts({int? since, bool preserveExisting = false}) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
@@ -2395,7 +2595,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(buildGetContactByKeyFrame(pubKey));
|
||||
}
|
||||
|
||||
Future<void> sendMessage(Contact contact, String text) async {
|
||||
Future<void> sendMessage(
|
||||
Contact contact,
|
||||
String text, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
}) async {
|
||||
if (!isConnected || text.isEmpty) return;
|
||||
|
||||
// Check if this is a reaction - apply locally with pending status and route through retry service
|
||||
@@ -2426,7 +2632,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
if (_retryService != null) {
|
||||
await _retryService!.sendMessageWithRetry(contact: contact, text: text);
|
||||
await _retryService!.sendMessageWithRetry(
|
||||
contact: contact,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
);
|
||||
} else {
|
||||
// Fallback to old behavior if retry service not initialized
|
||||
final resolved = resolvePathSelection(contact);
|
||||
@@ -2435,6 +2647,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
text,
|
||||
pathLength: resolved.useFlood ? -1 : resolved.hopCount,
|
||||
pathBytes: Uint8List.fromList(resolved.pathBytes),
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
);
|
||||
_addMessage(contact.publicKeyHex, message);
|
||||
notifyListeners();
|
||||
@@ -2740,7 +2955,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendChannelMessage(Channel channel, String text) async {
|
||||
Future<void> sendChannelMessage(
|
||||
Channel channel,
|
||||
String text, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
}) async {
|
||||
if (!isConnected || text.isEmpty) return;
|
||||
|
||||
// Check if this is a reaction - if so, process it immediately instead of adding as a message
|
||||
@@ -2787,18 +3008,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
text,
|
||||
_selfName ?? 'Me',
|
||||
channel.index,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
);
|
||||
_addChannelMessage(channel.index, message);
|
||||
_pendingChannelSentQueue.add(message.messageId);
|
||||
notifyListeners();
|
||||
|
||||
final trimmed = text.trim();
|
||||
final isStructuredPayload =
|
||||
trimmed.startsWith('g:') || trimmed.startsWith('m:');
|
||||
final outboundText =
|
||||
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||
? Smaz.encodeIfSmaller(text)
|
||||
: text;
|
||||
final outboundText = prepareChannelOutboundText(channel.index, text);
|
||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||
@@ -3725,7 +3943,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (mlTimeout != null) {
|
||||
if (pathLength < 0) {
|
||||
// Flood: trust ML, only enforce firmware formula as floor
|
||||
return mlTimeout.clamp(physicsMin, mlTimeout);
|
||||
if (mlTimeout < physicsMin) {
|
||||
return physicsMin;
|
||||
}
|
||||
}
|
||||
return mlTimeout.clamp(physicsMin, physicsMax);
|
||||
}
|
||||
@@ -3735,8 +3955,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleContact(Uint8List frame, {bool isContact = true}) {
|
||||
final contact = Contact.fromFrame(frame);
|
||||
if (contact != null) {
|
||||
final contactTmp = Contact.fromFrame(frame);
|
||||
if (contactTmp != null) {
|
||||
if (listEquals(contactTmp.publicKey, _selfPublicKey)) {
|
||||
appLogger.info(
|
||||
'Ignoring contact with self public key: ${contactTmp.name}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
removeContact(contactTmp);
|
||||
return;
|
||||
}
|
||||
final contact = getFromDiscovered(contactTmp);
|
||||
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
|
||||
|
||||
if (contact.type == advTypeRepeater) {
|
||||
@@ -3763,11 +3992,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||
// Preserve user-selected path settings and previously known GPS when
|
||||
// refreshed frames omit coordinates (lat/lon encoded as 0,0).
|
||||
_contacts[existingIndex] = contact.copyWith(
|
||||
lastMessageAt: mergedLastMessageAt,
|
||||
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||
pathOverrideBytes: existing.pathOverrideBytes,
|
||||
latitude: contact.latitude ?? existing.latitude,
|
||||
longitude: contact.longitude ?? existing.longitude,
|
||||
);
|
||||
|
||||
appLogger.info(
|
||||
@@ -3787,7 +4019,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
} else {
|
||||
appLogger.info(
|
||||
"Discovered contact ${contact.name} (type ${contact.typeLabel}) not added due to auto-add settings",
|
||||
"Discovered contact ${contact.name} (type ${contact.typeLabelRaw}) not added due to auto-add settings",
|
||||
tag: 'Connector',
|
||||
);
|
||||
return;
|
||||
@@ -3809,7 +4041,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactType: contact.typeLabelRaw,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
@@ -3884,7 +4116,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactType: contact.typeLabelRaw,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
@@ -3907,7 +4139,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (_contacts.isEmpty) return 0;
|
||||
var latest = 0;
|
||||
for (final contact in _contacts) {
|
||||
final seconds = contact.lastSeen.millisecondsSinceEpoch ~/ 1000;
|
||||
// prefer lastmod per spec, fallback to lastseen
|
||||
final source = contact.lastModified ?? contact.lastSeen;
|
||||
final seconds = source.millisecondsSinceEpoch ~/ 1000;
|
||||
if (seconds > latest) {
|
||||
latest = seconds;
|
||||
}
|
||||
@@ -4061,6 +4295,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
_addMessage(message.senderKeyHex, message);
|
||||
if (!message.isOutgoing) {
|
||||
unawaited(
|
||||
_translateIncomingContactMessage(message.senderKeyHex, message),
|
||||
);
|
||||
}
|
||||
_maybeIncrementContactUnread(message);
|
||||
notifyListeners();
|
||||
|
||||
@@ -4231,6 +4470,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return text;
|
||||
}
|
||||
|
||||
String prepareChannelOutboundText(int channelIndex, String text) {
|
||||
final trimmed = text.trim();
|
||||
final isStructuredPayload =
|
||||
trimmed.startsWith('g:') || trimmed.startsWith('m:');
|
||||
if (!isStructuredPayload && isChannelSmazEnabled(channelIndex)) {
|
||||
return Smaz.encodeIfSmaller(text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
String _channelDisplayName(int channelIndex) {
|
||||
for (final channel in _channels) {
|
||||
if (channel.index != channelIndex) continue;
|
||||
@@ -4283,6 +4532,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(message.channelIndex!, message);
|
||||
if (isNew && !message.isOutgoing) {
|
||||
unawaited(
|
||||
_translateIncomingChannelMessage(message.channelIndex!, message),
|
||||
);
|
||||
}
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
@@ -4362,6 +4616,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(channel.index, message);
|
||||
if (isNew && !message.isOutgoing) {
|
||||
unawaited(_translateIncomingChannelMessage(channel.index, message));
|
||||
}
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
@@ -5056,6 +5313,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
senderKey: message.senderKey,
|
||||
senderName: message.senderName,
|
||||
text: replyInfo.actualMessage,
|
||||
originalText: message.originalText,
|
||||
translatedText: message.translatedText,
|
||||
translatedLanguageCode: message.translatedLanguageCode,
|
||||
translationStatus: message.translationStatus,
|
||||
translationModelId: message.translationModelId,
|
||||
timestamp: message.timestamp,
|
||||
isOutgoing: message.isOutgoing,
|
||||
status: message.status,
|
||||
@@ -5787,7 +6049,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactType: contact.typeLabelRaw,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,15 +202,15 @@ const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSendTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdGetStats = 56;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
const int cmdGetAutoAddConfig = 59;
|
||||
const int cmdSetPathHashMode = 61;
|
||||
const int cmdGetStats = 56;
|
||||
|
||||
// Text message types
|
||||
const int txtTypePlain = 0;
|
||||
@@ -320,7 +320,7 @@ const int maxPathSize = 64;
|
||||
const int pathHashSize = 1;
|
||||
const int maxNameSize = 32;
|
||||
const int maxFrameSize = 172;
|
||||
const int appProtocolVersion = 3;
|
||||
const int appProtocolVersion = 4;
|
||||
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
||||
const int maxTextPayloadBytes = 160;
|
||||
const int _sendTextMsgOverheadBytes =
|
||||
@@ -735,10 +735,15 @@ Uint8List buildUpdateContactPathFrame(
|
||||
writer.writeInt32LE((longitude * 1e6).round());
|
||||
}
|
||||
|
||||
if (lastModified != null) {
|
||||
// Last modified
|
||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||
final hasLocation = lat != null && lon != null;
|
||||
if (hasLocation || lastModified != null) {
|
||||
writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0);
|
||||
writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0);
|
||||
if (lastModified != null) {
|
||||
// Last modified
|
||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
return writer.toBytes();
|
||||
|
||||
@@ -7,6 +7,9 @@ class MeshCoreUuids {
|
||||
"MeshCore-",
|
||||
"Whisper-",
|
||||
"WisCore-",
|
||||
"Seeed",
|
||||
"Lilygo",
|
||||
"HT-",
|
||||
"LowMesh_MC_",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -49,6 +49,25 @@ class ChatScrollController extends ScrollController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Jumps toward an off-screen message so that lazy ListView.builder builds
|
||||
/// items near it. Only visible + cacheExtent items have real heights, so we
|
||||
/// use proportion of maxScrollExtent (itself an estimate from built items'
|
||||
/// avg height). Call [onJumped] on the next frame to ensureVisible/scroll
|
||||
/// to the exact target.
|
||||
void jumpToEstimatedOffset({
|
||||
required int unreadCount,
|
||||
required int totalMessages,
|
||||
required VoidCallback onJumped,
|
||||
}) {
|
||||
if (!hasClients || totalMessages == 0) return;
|
||||
final maxExtent = position.maxScrollExtent;
|
||||
final jumpOffset = maxExtent * (unreadCount / totalMessages);
|
||||
if (jumpOffset > 100) {
|
||||
jumpTo(jumpOffset);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onJumped());
|
||||
}
|
||||
|
||||
void scrollToBottomIfAtBottom() {
|
||||
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
||||
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
class GifHelper {
|
||||
/// Parse a known GIF format, which can be any of:
|
||||
/// g:GIFID
|
||||
/// https://media.giphy.com/media/GIFID/giphy.gif
|
||||
/// https://giphy.com/gifs/Optional-title-with-dashes-GIFID
|
||||
///
|
||||
/// GIFID is a Giphy GIF ID. The https:// is optional (and
|
||||
/// can also be http://). The giphy.com/gifs form can also
|
||||
/// include a trailing slash.
|
||||
///
|
||||
/// Returns null if text is not a valid GIF format
|
||||
static String? parseGif(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
if (match != null) {
|
||||
return match.group(1);
|
||||
}
|
||||
final directUrlMatch = RegExp(
|
||||
r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$',
|
||||
).firstMatch(trimmed);
|
||||
if (directUrlMatch != null) {
|
||||
return directUrlMatch.group(1);
|
||||
}
|
||||
// Giphy understands page URLs with just the ID, or any string and a
|
||||
// dash before the ID, and redirects to a page with a dash-separated
|
||||
// title, a dash, and the ID. IDs in this form *probably* can't
|
||||
// contain dashes.
|
||||
final pageMatch = RegExp(
|
||||
r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$',
|
||||
).firstMatch(trimmed);
|
||||
return pageMatch?.group(1);
|
||||
}
|
||||
|
||||
/// Encode a GIF in a format that parseGif() can parse.
|
||||
static String encodeGif(String gifId) {
|
||||
return 'g:$gifId';
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class LinkHandler {
|
||||
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
|
||||
@@ -93,21 +94,19 @@ class LinkHandler {
|
||||
final uri = Uri.parse(url);
|
||||
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_invalidLink),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_invalidLink),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +109,9 @@ class ReactionHelper {
|
||||
|
||||
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||
}
|
||||
|
||||
/// Encode a reaction message that parseReaction() can parse.
|
||||
static String encodeReaction(String hash, String emojiIndex) {
|
||||
return 'r:$hash:$emojiIndex';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss
|
||||
// all other properties are default and optional
|
||||
void showDismissibleSnackBar(
|
||||
BuildContext context, {
|
||||
Key? key,
|
||||
required Widget content,
|
||||
Color? backgroundColor,
|
||||
double? elevation,
|
||||
EdgeInsetsGeometry? margin,
|
||||
EdgeInsetsGeometry? padding,
|
||||
double? width,
|
||||
ShapeBorder? shape,
|
||||
HitTestBehavior? hitTestBehavior,
|
||||
SnackBarBehavior? behavior,
|
||||
SnackBarAction? action,
|
||||
double? actionOverflowThreshold,
|
||||
bool? showCloseIcon,
|
||||
Color? closeIconColor,
|
||||
Duration? duration,
|
||||
bool? persist,
|
||||
Animation<double>? animation,
|
||||
void Function()? onVisible,
|
||||
DismissDirection? dismissDirection,
|
||||
Clip? clipBehavior,
|
||||
}) {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
key: key,
|
||||
content: GestureDetector(
|
||||
onTap: () => messenger.hideCurrentSnackBar(),
|
||||
child: content,
|
||||
),
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
width: width,
|
||||
shape: shape,
|
||||
hitTestBehavior: hitTestBehavior,
|
||||
behavior: behavior,
|
||||
action: action,
|
||||
actionOverflowThreshold: actionOverflowThreshold,
|
||||
showCloseIcon: showCloseIcon,
|
||||
closeIconColor: closeIconColor,
|
||||
duration: duration ?? const Duration(seconds: 4),
|
||||
persist: persist,
|
||||
animation: animation,
|
||||
onVisible: onVisible,
|
||||
dismissDirection: dismissDirection ?? DismissDirection.down,
|
||||
clipBehavior: clipBehavior ?? Clip.hardEdge,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,14 @@ import 'package:flutter/services.dart';
|
||||
|
||||
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
final int maxBytes;
|
||||
final String Function(String)? encoder;
|
||||
|
||||
const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
|
||||
const Utf8LengthLimitingTextInputFormatter(this.maxBytes, {this.encoder});
|
||||
|
||||
int _effectiveByteLength(String text) {
|
||||
final effective = encoder != null ? encoder!(text) : text;
|
||||
return utf8.encode(effective).length;
|
||||
}
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
@@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (maxBytes <= 0) return oldValue;
|
||||
final bytes = utf8.encode(newValue.text);
|
||||
if (bytes.length <= maxBytes) return newValue;
|
||||
if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue;
|
||||
|
||||
final truncated = _truncateToMaxBytes(newValue.text, maxBytes);
|
||||
return TextEditingValue(
|
||||
@@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
}
|
||||
|
||||
String _truncateToMaxBytes(String text, int limit) {
|
||||
if (encoder != null) {
|
||||
final runes = text.runes.toList();
|
||||
while (runes.isNotEmpty &&
|
||||
_effectiveByteLength(String.fromCharCodes(runes)) > maxBytes) {
|
||||
runes.removeLast();
|
||||
}
|
||||
return String.fromCharCodes(runes);
|
||||
}
|
||||
final buffer = StringBuffer();
|
||||
var used = 0;
|
||||
for (final rune in text.runes) {
|
||||
|
||||
+143
-11
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Режим на поверителност е активиран",
|
||||
"settings_privacyModeDisabled": "Режим на поверителност е деактивиран",
|
||||
"settings_actions": "Действия",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Изпрати Реклама",
|
||||
"settings_sendAdvertisementSubtitle": "Сега присъствие в ефир",
|
||||
"settings_advertisementSent": "Реклама изпратена",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
|
||||
"contact_teleLoc": "Местоположение на телеметрията",
|
||||
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
|
||||
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
|
||||
@@ -1940,7 +1935,6 @@
|
||||
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Мулти-потвърди: {value}",
|
||||
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
|
||||
"map_showOverlaps": "Покриване на ключа на повтаряча",
|
||||
"map_runTraceWithReturnPath": "Върни се по същия път.",
|
||||
@@ -2007,6 +2001,34 @@
|
||||
"radioStats_stripWaiting": "Извличане на данни за радиото…",
|
||||
"radioStats_settingsTile": "Статистически данни за радиостанции",
|
||||
"radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableTitle": "Активирайте превода",
|
||||
"translation_title": "Превод",
|
||||
"translation_composerTitle": "Преведете преди да изпратите",
|
||||
"translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.",
|
||||
"translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.",
|
||||
"translation_targetLanguage": "Целеви език",
|
||||
"translation_useAppLanguage": "Използвайте езика на приложението",
|
||||
"translation_downloadedModelLabel": "Изтегнат модел",
|
||||
"translation_presetModelLabel": "Предварително конфигуриран модел от Hugging Face",
|
||||
"translation_manualUrlLabel": "URL на ръководството",
|
||||
"translation_downloadModel": "Изтеглете модела",
|
||||
"translation_downloading": "Изтегляне...",
|
||||
"translation_working": "Работа...",
|
||||
"translation_stop": "Спрете",
|
||||
"translation_mergingChunks": "Съединяване на изтеглените части в един файл...",
|
||||
"translation_downloadedModels": "Изтеглени модели",
|
||||
"translation_deleteModel": "Изтриване на модела",
|
||||
"translation_modelDownloaded": "Моделът за превод е изтеглен.",
|
||||
"translation_downloadStopped": "Изтеглянето беше прекъснато.",
|
||||
"translation_downloadFailed": "Не успях да изтегля: {error}",
|
||||
"translation_enterUrlFirst": "Въведете първо URL адрес на модела.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2036,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingHidePin": "Скрий ПИН",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Изпращайте съобщения на оригиналния въведен език.",
|
||||
"translation_translateBeforeSending": "Преведете преди да изпратите",
|
||||
"translation_messageTranslation": "Превод на съобщението",
|
||||
"translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.",
|
||||
"translation_translateTo": "Превеждане на {language}",
|
||||
"translation_translationOptions": "Опции за превод",
|
||||
"translation_systemLanguage": "Език на системата",
|
||||
"scanner_linuxPairingPinTitle": "PIN за съвпадение чрез Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв).",
|
||||
"scanner_linuxPairingHidePin": "Скриване на PIN кода",
|
||||
"scanner_linuxPairingShowPin": "Покажи PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма)."
|
||||
"repeater_cliQuickClockSync": "Синхронизация на часовника",
|
||||
"repeater_cliQuickDiscovery": "Открий Съседи",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.",
|
||||
"repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане",
|
||||
"chat_sendMessage": "Изпратете съобщение",
|
||||
"room_guest": "Информация за сървъра на стаята",
|
||||
"repeater_guest": "Информация за ретранслаторите",
|
||||
"repeater_guestTools": "Инструменти за гости",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Множество потвърждения",
|
||||
"map_sharedAt": "Споделено",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Избрано препятствие",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsHint": "Кликнете върху блокираната точка, за да я отбележите на картата.",
|
||||
"losBlockedSpotsTitle": "Ограничени места",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+141
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
|
||||
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
|
||||
"settings_actions": "Aktionen",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Sende Ankündigung",
|
||||
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
|
||||
"settings_advertisementSent": "Ankündigung gesendet",
|
||||
@@ -1950,13 +1952,6 @@
|
||||
"contact_lastSeen": "Zuletzt gesehen",
|
||||
"contact_clearChat": "Chat löschen",
|
||||
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
|
||||
"appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
|
||||
@@ -1969,7 +1964,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
|
||||
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
|
||||
"map_showOverlaps": "Überlappungen der Repeater-Taste",
|
||||
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2035,6 +2029,34 @@
|
||||
"radioStats_stripWaiting": "Abrufen von Radiostatus…",
|
||||
"radioStats_settingsTile": "Senderinformationen",
|
||||
"radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "Übersetzung",
|
||||
"translation_composerTitle": "Übersetzen Sie vor dem Versenden",
|
||||
"translation_enableSubtitle": "Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.",
|
||||
"translation_enableTitle": "Aktivieren Sie die Übersetzung",
|
||||
"translation_composerSubtitle": "Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.",
|
||||
"translation_targetLanguage": "Zielsprache",
|
||||
"translation_useAppLanguage": "Verwenden Sie die App-Sprache",
|
||||
"translation_downloadedModelLabel": "Heruntergeladenes Modell",
|
||||
"translation_presetModelLabel": "Vordefinierter Hugging Face-Modell",
|
||||
"translation_manualUrlLabel": "URL für das manuelle Modell",
|
||||
"translation_downloadModel": "Modell herunterladen",
|
||||
"translation_downloading": "Herunterladen...",
|
||||
"translation_working": "Arbeiten...",
|
||||
"translation_stop": "Stopp",
|
||||
"translation_mergingChunks": "Zusammenführen der heruntergeladenen Teile in die finale Datei...",
|
||||
"translation_downloadedModels": "Heruntergeladene Modelle",
|
||||
"translation_deleteModel": "Modell löschen",
|
||||
"translation_modelDownloaded": "Übersetzungsmotor heruntergeladen.",
|
||||
"translation_downloadStopped": "Herunterladen abgebrochen.",
|
||||
"translation_downloadFailed": "Download fehlgeschlagen: {error}",
|
||||
"translation_enterUrlFirst": "Geben Sie zunächst die URL eines Modells ein.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2042,8 +2064,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Nachricht übersetzen",
|
||||
"translation_composerEnabledHint": "Die Nachrichten werden vor dem Versenden übersetzt.",
|
||||
"translation_translateBeforeSending": "Übersetzen Sie vor dem Versenden",
|
||||
"translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.",
|
||||
"translation_translateTo": "Übersetzen Sie auf {language}",
|
||||
"translation_translationOptions": "Übersetzungsmöglichkeiten",
|
||||
"translation_systemLanguage": "Sprache des Systems",
|
||||
"scanner_linuxPairingShowPin": "PIN anzeigen",
|
||||
"scanner_linuxPairingHidePin": "PIN ausblenden",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine)."
|
||||
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).",
|
||||
"repeater_cliQuickClockSync": "Uhr Synchronisieren",
|
||||
"repeater_cliQuickDiscovery": "Entdecke Nachbarn",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Uhrzeit-Synchronisation nach dem Anmelden",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.",
|
||||
"repeater_guest": "Informationen zu Repeatern",
|
||||
"repeater_guestTools": "Gastwerkzeuge",
|
||||
"chat_sendMessage": "Nachricht senden",
|
||||
"room_guest": "Informationen zum Room Server",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Mehrere Bestätigungen",
|
||||
"map_sharedAt": "Geteilt",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Reservierte Plätze",
|
||||
"losSelectedObstructionTitle": "Ausgewählte Behinderung",
|
||||
"losBlockedSpotChip": "{distance} • {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsHint": "Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.",
|
||||
"losSelectedObstructionDetails": "Blockiert durch {obstruction} in einer Höhe von {heightUnit}, {distanceFromA} von A und {distanceFromB} von B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+170
-16
@@ -12,6 +12,7 @@
|
||||
"common_delete": "Delete",
|
||||
"common_deleteAll": "Delete All",
|
||||
"common_close": "Close",
|
||||
"common_done": "Done",
|
||||
"common_edit": "Edit",
|
||||
"common_add": "Add",
|
||||
"common_settings": "Settings",
|
||||
@@ -178,16 +179,11 @@
|
||||
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
|
||||
"settings_advertLocation": "Advert Location",
|
||||
"settings_advertLocationSubtitle": "Include location in advert.",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_multiAck": "Multi-ACKs",
|
||||
"settings_telemetryModeUpdated": "Telemetry mode updated",
|
||||
"settings_actions": "Actions",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Send Advertisement",
|
||||
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
|
||||
"settings_advertisementSent": "Advertisement sent",
|
||||
@@ -200,8 +196,8 @@
|
||||
"settings_rebootDeviceSubtitle": "Restart the MeshCore device",
|
||||
"settings_rebootDeviceConfirm": "Are you sure you want to reboot the device? You will be disconnected.",
|
||||
"settings_debug": "Debug",
|
||||
"settings_bleDebugLog": "BLE Debug Log",
|
||||
"settings_bleDebugLogSubtitle": "BLE commands, responses, and raw data",
|
||||
"settings_companionDebugLog": "Companion Debug Log",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commands, responses, and raw data",
|
||||
"settings_appDebugLog": "App Debug Log",
|
||||
"settings_appDebugLogSubtitle": "Application debug messages",
|
||||
"settings_about": "About",
|
||||
@@ -303,8 +299,12 @@
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": { "type": "String" },
|
||||
"max": { "type": "String" }
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_battery": "Battery",
|
||||
@@ -519,6 +519,14 @@
|
||||
},
|
||||
"channels_hashtagChannel": "Hashtag channel",
|
||||
"channels_public": "Public",
|
||||
"channels_via": "via {path}",
|
||||
"@channels_via": {
|
||||
"placeholders": {
|
||||
"path": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_private": "Private",
|
||||
"channels_publicChannel": "Public channel",
|
||||
"channels_privateChannel": "Private channel",
|
||||
@@ -603,6 +611,15 @@
|
||||
"channels_enterHashtag": "Enter hashtag",
|
||||
"channels_hashtagHint": "e.g. #team",
|
||||
"chat_noMessages": "No messages yet",
|
||||
"chat_sendMessage": "Send message",
|
||||
"chat_sendMessageTo": "Send message to {name}",
|
||||
"@chat_sendMessageTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendMessageToStart": "Send a message to get started",
|
||||
"chat_originalMessageNotFound": "Original message not found",
|
||||
"chat_replyingTo": "Replying to {name}",
|
||||
@@ -760,6 +777,7 @@
|
||||
}
|
||||
},
|
||||
"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:",
|
||||
@@ -811,6 +829,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_markAsUnread": "Mark as Unread",
|
||||
"chat_newMessages": "New messages",
|
||||
"chat_openLink": "Open Link?",
|
||||
"chat_openLinkConfirmation": "Do you want to open this link in your browser?",
|
||||
"chat_open": "Open",
|
||||
@@ -856,6 +876,12 @@
|
||||
"map_from": "From",
|
||||
"map_source": "Source",
|
||||
"map_flags": "Flags",
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"map_shareMarkerHere": "Share marker here",
|
||||
"map_setAsMyLocation": "Set as my location",
|
||||
"map_pinLabel": "Pin label",
|
||||
@@ -890,6 +916,7 @@
|
||||
"map_guessedLocation": "Guessed location",
|
||||
"map_lastSeenTime": "Last Seen Time",
|
||||
"map_sharedPin": "Shared pin",
|
||||
"map_sharedAt": "Shared",
|
||||
"map_joinRoom": "Join Room",
|
||||
"map_manageRepeater": "Manage Repeater",
|
||||
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
||||
@@ -1025,8 +1052,8 @@
|
||||
"login_enterPassword": "Enter password",
|
||||
"login_savePassword": "Save password",
|
||||
"login_savePasswordSubtitle": "Password will be stored securely on this device",
|
||||
"login_repeaterDescription": "Enter the repeater password to access settings and status.",
|
||||
"login_roomDescription": "Enter the room password to access settings and status.",
|
||||
"login_repeaterDescription": "Enter the repeater password for guest or admin access.",
|
||||
"login_roomDescription": "Enter the room password for guest or admin access.",
|
||||
"login_routing": "Routing",
|
||||
"login_routingMode": "Routing mode",
|
||||
"login_autoUseSavedPath": "Auto (use saved path)",
|
||||
@@ -1092,7 +1119,10 @@
|
||||
"path_setPath": "Set Path",
|
||||
"repeater_management": "Repeater Management",
|
||||
"room_management": "Room Server Management",
|
||||
"repeater_guest": "Repeater Information",
|
||||
"room_guest": "Room Server Information",
|
||||
"repeater_managementTools": "Management Tools",
|
||||
"repeater_guestTools": "Guest Tools",
|
||||
"repeater_status": "Status",
|
||||
"repeater_statusSubtitle": "View repeater status, stats, and neighbors",
|
||||
"repeater_telemetry": "Telemetry",
|
||||
@@ -1103,6 +1133,14 @@
|
||||
"repeater_neighborsSubtitle": "View zero hop neighbors.",
|
||||
"repeater_settings": "Settings",
|
||||
"repeater_settingsSubtitle": "Configure repeater parameters",
|
||||
"repeater_clockSyncAfterLogin": "Clock sync after login",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatically send \"clock sync\" after a successful login",
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_statusTitle": "Repeater Status",
|
||||
"repeater_routingMode": "Routing mode",
|
||||
"repeater_autoUseSavedPath": "Auto (use saved path)",
|
||||
@@ -1333,6 +1371,8 @@
|
||||
"repeater_cliQuickVersion": "Version",
|
||||
"repeater_cliQuickAdvertise": "Advertise",
|
||||
"repeater_cliQuickClock": "Clock",
|
||||
"repeater_cliQuickClockSync": "Clock Sync",
|
||||
"repeater_cliQuickDiscovery": "Discover Neighbors",
|
||||
"repeater_cliHelpAdvert": "Sends an advertisement packet",
|
||||
"repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)",
|
||||
"repeater_cliHelpClock": "Displays current time per device's clock.",
|
||||
@@ -1856,6 +1896,46 @@
|
||||
"losLegendRadioHorizon": "Radio horizon",
|
||||
"losLegendLosBeam": "LOS beam",
|
||||
"losLegendTerrain": "Terrain",
|
||||
"losBlockedSpotsTitle": "Blocked spots",
|
||||
"losBlockedSpotsHint": "Tap a blocked spot to highlight it on the map.",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Selected obstruction",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losFrequencyLabel": "Frequency",
|
||||
"losFrequencyInfoTooltip": "View calculation details",
|
||||
"losFrequencyDialogTitle": "Radio horizon calculation",
|
||||
@@ -2042,6 +2122,35 @@
|
||||
"radioStats_stripWaiting": "Fetching radio stats…",
|
||||
"radioStats_settingsTile": "Radio stats",
|
||||
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
|
||||
|
||||
"translation_title": "Translation",
|
||||
"translation_enableTitle": "Enable translation",
|
||||
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
|
||||
"translation_composerTitle": "Translate before sending",
|
||||
"translation_composerSubtitle": "Controls the default state of the composer translation icon.",
|
||||
"translation_targetLanguage": "Target language",
|
||||
"translation_useAppLanguage": "Use app language",
|
||||
"translation_downloadedModelLabel": "Downloaded model",
|
||||
"translation_presetModelLabel": "Preset Hugging Face model",
|
||||
"translation_manualUrlLabel": "Manual model URL",
|
||||
"translation_downloadModel": "Download model",
|
||||
"translation_downloading": "Downloading...",
|
||||
"translation_working": "Working...",
|
||||
"translation_stop": "Stop",
|
||||
"translation_mergingChunks": "Merging downloaded chunks into final file...",
|
||||
"translation_downloadedModels": "Downloaded models",
|
||||
"translation_deleteModel": "Delete model",
|
||||
"translation_modelDownloaded": "Translation model downloaded.",
|
||||
"translation_downloadStopped": "Download stopped.",
|
||||
"translation_downloadFailed": "Download failed: {error}",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enterUrlFirst": "Enter a model URL first.",
|
||||
"scanner_linuxPairingShowPin": "Show PIN",
|
||||
"scanner_linuxPairingHidePin": "Hide PIN",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN",
|
||||
@@ -2052,5 +2161,50 @@
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Message translation",
|
||||
"translation_translateBeforeSending": "Translate before sending",
|
||||
"translation_composerEnabledHint": "Messages will be translated before send.",
|
||||
"translation_composerDisabledHint": "Send messages in the original typed language.",
|
||||
"translation_translateTo": "Translate to {language}",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translationOptions": "Translation options",
|
||||
"translation_systemLanguage": "System language",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown"
|
||||
}
|
||||
|
||||
+143
-11
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Modo de privacidad activado",
|
||||
"settings_privacyModeDisabled": "Modo de privacidad desactivado",
|
||||
"settings_actions": "Acciones",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Enviar Anuncio",
|
||||
"settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora",
|
||||
"settings_advertisementSent": "Anuncio enviado",
|
||||
@@ -1950,13 +1952,6 @@
|
||||
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
|
||||
"contact_teleEnv": "Entorno de Telemetría",
|
||||
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso inicial de la ruta",
|
||||
"appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
|
||||
@@ -1969,7 +1964,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Superposiciones de tecla repetidora",
|
||||
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2035,6 +2029,34 @@
|
||||
"radioStats_stripWaiting": "Obteniendo estadísticas de la radio…",
|
||||
"radioStats_settingsTile": "Estadísticas de radio",
|
||||
"radioStats_settingsSubtitle": "Nivel de ruido, RSSI, SNR y tiempo de transmisión",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "Traducción",
|
||||
"translation_enableSubtitle": "Traducir los mensajes entrantes y permitir la traducción previa al envío.",
|
||||
"translation_enableTitle": "Habilitar la traducción",
|
||||
"translation_composerTitle": "Traducir antes de enviar",
|
||||
"translation_composerSubtitle": "Controla el estado predeterminado del icono de traducción del compositor.",
|
||||
"translation_targetLanguage": "Idioma de destino",
|
||||
"translation_useAppLanguage": "Utilizar el idioma de la aplicación",
|
||||
"translation_downloadedModelLabel": "Modelo descargado",
|
||||
"translation_presetModelLabel": "Modelo predefinido de Hugging Face",
|
||||
"translation_manualUrlLabel": "URL del modelo manual",
|
||||
"translation_downloadModel": "Descargar el modelo",
|
||||
"translation_downloading": "Descargando...",
|
||||
"translation_working": "Trabajando...",
|
||||
"translation_stop": "¡Detente!",
|
||||
"translation_mergingChunks": "Combinando los fragmentos descargados en el archivo final...",
|
||||
"translation_downloadedModels": "Modelos descargados",
|
||||
"translation_deleteModel": "Eliminar modelo",
|
||||
"translation_modelDownloaded": "Modelo de traducción descargado.",
|
||||
"translation_downloadStopped": "La descarga se ha detenido.",
|
||||
"translation_downloadFailed": "No se pudo descargar: {error}",
|
||||
"translation_enterUrlFirst": "Primero, introduzca la URL del modelo.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2042,8 +2064,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingShowPin": "Mostrar PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).",
|
||||
"scanner_linuxPairingShowPin": "Mostrar código PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth",
|
||||
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno)."
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Envía mensajes utilizando el lenguaje escrito original.",
|
||||
"translation_composerEnabledHint": "Los mensajes serán traducidos antes de ser enviados.",
|
||||
"translation_messageTranslation": "Traducción del mensaje",
|
||||
"translation_translateBeforeSending": "Traducir antes de enviar",
|
||||
"translation_translateTo": "Traducir a {language}",
|
||||
"translation_translationOptions": "Opciones de traducción",
|
||||
"translation_systemLanguage": "Idioma del sistema",
|
||||
"repeater_cliQuickDiscovery": "Descubrir Vecinos",
|
||||
"repeater_cliQuickClockSync": "Sincronización del reloj",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.",
|
||||
"repeater_clockSyncAfterLogin": "Sincronización del reloj después de iniciar sesión",
|
||||
"repeater_guest": "Información sobre repetidores",
|
||||
"chat_sendMessage": "Enviar mensaje",
|
||||
"repeater_guestTools": "Herramientas para invitados",
|
||||
"room_guest": "Información del servidor",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Múltiples respuestas de confirmación",
|
||||
"map_sharedAt": "Compartido",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Espacios ocupados",
|
||||
"losBlockedSpotsHint": "Seleccione un punto bloqueado para resaltarlo en el mapa.",
|
||||
"losSelectedObstructionTitle": "Obstrucción seleccionada",
|
||||
"losSelectedObstructionDetails": "Bloqueado por {obstruction} a una altura de {heightUnit}, a {distanceFromA} metros de A y a {distanceFromB} metros de B ({distanceUnit}).",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||
}
|
||||
|
||||
+166
-34
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Mode de confidentialité activé",
|
||||
"settings_privacyModeDisabled": "Mode de confidentialité désactivé",
|
||||
"settings_actions": "Actions",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "S'annoncer",
|
||||
"settings_sendAdvertisementSubtitle": "Présence diffusée maintenant",
|
||||
"settings_advertisementSent": "Annonce envoyée",
|
||||
@@ -143,8 +145,8 @@
|
||||
"settings_frequencyHelper": "300,0 - 2 500,0",
|
||||
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
||||
"settings_bandwidth": "Bande passante",
|
||||
"settings_spreadingFactor": "Facteur de répartition",
|
||||
"settings_codingRate": "Taux de codage",
|
||||
"settings_spreadingFactor": "Facteur de répartition (SF)",
|
||||
"settings_codingRate": "Taux de codage (CR)",
|
||||
"settings_txPower": "TX Puissance (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
||||
@@ -567,7 +569,7 @@
|
||||
"chat_clearPath": "Effacer le chemin",
|
||||
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
|
||||
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
|
||||
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
|
||||
"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.",
|
||||
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
|
||||
"chat_fullPath": "Chemin complet",
|
||||
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
|
||||
@@ -643,7 +645,7 @@
|
||||
},
|
||||
"map_chat": "Chat",
|
||||
"map_repeater": "Répéteur",
|
||||
"map_room": "Salle",
|
||||
"map_room": "Room Server",
|
||||
"map_sensor": "Capteur",
|
||||
"map_pinDm": "Clé (DM)",
|
||||
"map_pinPrivate": "Verrouiller (Privé)",
|
||||
@@ -682,7 +684,7 @@
|
||||
"map_showSharedMarkers": "Afficher les marqueurs partagés",
|
||||
"map_lastSeenTime": "Dernière fois vu",
|
||||
"map_sharedPin": "Clé partagée",
|
||||
"map_joinRoom": "Rejoindre la salle",
|
||||
"map_joinRoom": "Rejoindre le room server",
|
||||
"map_manageRepeater": "Gérer le répéteur",
|
||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
||||
@@ -865,7 +867,7 @@
|
||||
"path_labelHexPrefixes": "Préfixes hexadécimaux",
|
||||
"path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)",
|
||||
"path_selectFromContacts": "Sélectionner à partir des contacts :",
|
||||
"path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.",
|
||||
"path_noRepeatersFound": "Aucun répéteur ou room server n'a été trouvé.",
|
||||
"path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.",
|
||||
"path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}",
|
||||
"@path_invalidHexPrefixes": {
|
||||
@@ -996,15 +998,15 @@
|
||||
"repeater_txPower": "TX Puissance",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "Bande passante",
|
||||
"repeater_spreadingFactor": "Facteur de répartition",
|
||||
"repeater_codingRate": "Taux de codage",
|
||||
"repeater_spreadingFactor": "Facteur de répartition (SF)",
|
||||
"repeater_codingRate": "Taux de codage (CR)",
|
||||
"repeater_locationSettings": "Paramètres de localisation",
|
||||
"repeater_latitude": "Latitude",
|
||||
"repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)",
|
||||
"repeater_longitude": "Longitude",
|
||||
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
||||
"repeater_features": "Fonctionnalités",
|
||||
"repeater_packetForwarding": "Transfert de paquets",
|
||||
"repeater_packetForwarding": "Mode répéteur",
|
||||
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
||||
"repeater_guestAccess": "Accès Invité",
|
||||
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
||||
@@ -1377,7 +1379,7 @@
|
||||
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
|
||||
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
|
||||
"channels_scanQrCode": "Scanner un code QR",
|
||||
"channels_scanQrCode": "Scanner un QR code",
|
||||
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
||||
"channels_enterHashtag": "Entrez le hashtag",
|
||||
"channels_hashtagHint": "ex. #equipe",
|
||||
@@ -1466,8 +1468,8 @@
|
||||
"community_join": "Rejoindre",
|
||||
"community_joinTitle": "Rejoindre la communauté",
|
||||
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
|
||||
"community_scanQr": "Scanner la communauté QR",
|
||||
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
|
||||
"community_scanQr": "Scanner un QR code de communauté",
|
||||
"community_scanInstructions": "Pointez l'appareil photo vers un QR code de communauté.",
|
||||
"community_showQr": "Afficher le QR Code",
|
||||
"community_publicChannel": "Communauté Publique",
|
||||
"community_hashtagChannel": "Hashtag Communauté",
|
||||
@@ -1478,13 +1480,13 @@
|
||||
"community_qrTitle": "Partager Communauté",
|
||||
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
|
||||
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
|
||||
"community_invalidQrCode": "Code QR de communauté non valide",
|
||||
"community_invalidQrCode": "QR code de communauté non valide",
|
||||
"community_alreadyMember": "Déjà membre",
|
||||
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
|
||||
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
|
||||
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
|
||||
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
|
||||
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
|
||||
"community_scanOrCreate": "Scanner un QR code ou créer une communauté pour commencer",
|
||||
"community_manageCommunities": "Gérer les Communautés",
|
||||
"community_delete": "Quitter la communauté",
|
||||
"community_deleteConfirm": "Quitter \"{name}\" ?",
|
||||
@@ -1534,10 +1536,10 @@
|
||||
}
|
||||
},
|
||||
"community_regenerateSecret": "Régénérer le secret",
|
||||
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
|
||||
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.",
|
||||
"community_regenerate": "Régénérer",
|
||||
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"{name}\"",
|
||||
"community_updateSecret": "Mettre à jour le secret",
|
||||
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
@@ -1554,11 +1556,11 @@
|
||||
"contacts_pathTrace": "Traçage de chemin",
|
||||
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
|
||||
"contacts_repeaterPing": "Pinguer le répéteur",
|
||||
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
|
||||
"contacts_roomPathTrace": "Traçage du chemin vers le room server",
|
||||
"contacts_chatTraceRoute": "Tracer le chemin",
|
||||
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_roomPing": "Pinguer le serveur de la salle",
|
||||
"contacts_roomPing": "Pinguer le room server",
|
||||
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||
"appSettings_languageUk": "Ukrainien",
|
||||
"appSettings_languageRu": "Russe",
|
||||
@@ -1583,12 +1585,12 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
||||
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
||||
"notification_receivedNewMessage": "Nouveau message reçu",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / room servers au format GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
||||
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
||||
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
|
||||
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
|
||||
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
|
||||
"settings_gpxExportRepeatersRoom": "Emplacements des répéteurs et room servers",
|
||||
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
|
||||
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
|
||||
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
|
||||
@@ -1800,15 +1802,15 @@
|
||||
"contacts_unread": "Non lu",
|
||||
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
|
||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} room server...",
|
||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||
"settings_contactSettings": "Paramètres de contact",
|
||||
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les room servers",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les room servers découverts",
|
||||
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
|
||||
"contactsSettings_title": "Paramètres des contacts",
|
||||
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_lastSeen": "Dernière fois vu",
|
||||
"contact_clearChat": "Effacer la conversation",
|
||||
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.",
|
||||
"appSettings_initialRouteWeight": "Poids initial de l'itinéraire",
|
||||
"appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
|
||||
@@ -1940,7 +1935,6 @@
|
||||
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Multi-ACKs : {value}",
|
||||
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
|
||||
"map_showOverlaps": "Chevauchement de la touche répétitive",
|
||||
"map_runTraceWithReturnPath": "Revenir sur le même chemin.",
|
||||
@@ -2007,6 +2001,34 @@
|
||||
"radioStats_stripWaiting": "Récupération des statistiques de la radio…",
|
||||
"radioStats_settingsTile": "Statistiques de radio",
|
||||
"radioStats_settingsSubtitle": "Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d'antenne",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Traduire avant d'envoyer",
|
||||
"translation_enableTitle": "Activer la traduction",
|
||||
"translation_title": "Traduction",
|
||||
"translation_enableSubtitle": "Traduire les messages entrants et permettre la traduction avant l'envoi.",
|
||||
"translation_composerSubtitle": "Contrôle l'état par défaut de l'icône de traduction du composant.",
|
||||
"translation_targetLanguage": "Langue cible",
|
||||
"translation_useAppLanguage": "Utiliser la langue de l'application",
|
||||
"translation_downloadedModelLabel": "Modèle téléchargé",
|
||||
"translation_presetModelLabel": "Modèle Hugging Face préconfiguré",
|
||||
"translation_manualUrlLabel": "URL du modèle manuel",
|
||||
"translation_downloadModel": "Télécharger le modèle",
|
||||
"translation_downloading": "Téléchargement...",
|
||||
"translation_working": "Au travail...",
|
||||
"translation_stop": "Arrêtez",
|
||||
"translation_mergingChunks": "Fusion des fragments téléchargés dans le fichier final...",
|
||||
"translation_downloadedModels": "Modèles téléchargés",
|
||||
"translation_deleteModel": "Supprimer le modèle",
|
||||
"translation_modelDownloaded": "Modèle de traduction téléchargé.",
|
||||
"translation_downloadStopped": "Le téléchargement a été interrompu.",
|
||||
"translation_downloadFailed": "Échec du téléchargement : {error}",
|
||||
"translation_enterUrlFirst": "Entrez d'abord l'URL du modèle.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2036,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingShowPin": "Afficher le code PIN",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerEnabledHint": "Les messages seront traduits avant d'être envoyés.",
|
||||
"translation_translateBeforeSending": "Traduire avant d'envoyer",
|
||||
"translation_composerDisabledHint": "Envoyez des messages dans la langue originale, telle que vous l'avez tapée.",
|
||||
"translation_messageTranslation": "Traduction du message",
|
||||
"translation_translateTo": "Traduire en {language}",
|
||||
"translation_translationOptions": "Options de traduction",
|
||||
"translation_systemLanguage": "Langue du système",
|
||||
"scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth",
|
||||
"scanner_linuxPairingHidePin": "Masquer le code PIN",
|
||||
"scanner_linuxPairingPinTitle": "Code PIN d’appairage Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun)."
|
||||
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
|
||||
"scanner_linuxPairingShowPin": "Afficher le code PIN",
|
||||
"repeater_cliQuickClockSync": "Synchronisation de l'horloge",
|
||||
"repeater_cliQuickDiscovery": "Découvrir les voisins",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Envoyer automatiquement une notification \"synchronisation de l'heure\" après une connexion réussie.",
|
||||
"repeater_clockSyncAfterLogin": "Synchronisation de l'horloge après la connexion",
|
||||
"repeater_guestTools": "Outils pour les invités",
|
||||
"chat_sendMessage": "Envoyer un message",
|
||||
"room_guest": "Informations sur le serveur",
|
||||
"repeater_guest": "Informations sur les répéteurs",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Plusieurs accusés de réception",
|
||||
"map_sharedAt": "Partagé",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Obstruction sélectionnée",
|
||||
"losBlockedSpotsTitle": "Places occupés",
|
||||
"losBlockedSpotsHint": "Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.",
|
||||
"losSelectedObstructionDetails": "Bloqué par {obstruction}, à une hauteur de {heightUnit}, à une distance de {distanceFromA} par rapport à A et à une distance de {distanceFromB} par rapport à B ({distanceUnit}).",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||
}
|
||||
|
||||
+143
-11
@@ -167,6 +167,8 @@
|
||||
"settings_privacyModeEnabled": "Adatvédelem mód beállítva",
|
||||
"settings_privacyModeDisabled": "Adatvédelem mód kikapcsolva",
|
||||
"settings_actions": "Tevékenységek",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Hirdetés küldése",
|
||||
"settings_sendAdvertisementSubtitle": "A nyilvános megjelenés",
|
||||
"settings_advertisementSent": "Hirdetés elküldve",
|
||||
@@ -2012,13 +2014,6 @@
|
||||
"radioStats_stripWaiting": "Rádió adatok begyűjtése…",
|
||||
"radioStats_settingsTile": "Rádió statisztikák",
|
||||
"radioStats_settingsSubtitle": "Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_denyAll": "Elutasítom",
|
||||
"settings_privacySettingsDescription": "Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.",
|
||||
"settings_privacySubtitle": "Ellenőrizd, hogy milyen információkat osztanak meg.",
|
||||
@@ -2030,7 +2025,6 @@
|
||||
"settings_telemetryEnvironmentMode": "Adatkapcsolati környezeti mód",
|
||||
"settings_advertLocation": "Reklám megjelenési hely",
|
||||
"settings_advertLocationSubtitle": "A hirdetés tartalmazza a helyszínt.",
|
||||
"settings_multiAck": "Többszöri visszaigazolások: {value}",
|
||||
"settings_telemetryModeUpdated": "A telemetriamód frissítve",
|
||||
"contact_info": "Kapcsolattartási információk",
|
||||
"contact_settings": "Kapcsolat beállítások",
|
||||
@@ -2045,6 +2039,34 @@
|
||||
"contact_teleEnvSubtitle": "Engedje meg az érzékelő adatok megosztását",
|
||||
"map_showOverlaps": "Az ismétlő kulcsok ütköznek",
|
||||
"map_runTraceWithReturnPath": "Visszaforduljon az eredeti úton.",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "Fordítás",
|
||||
"translation_enableTitle": "Engedje meg a fordítást",
|
||||
"translation_enableSubtitle": "Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.",
|
||||
"translation_composerTitle": "Fordítsa el, mielőtt elküldi",
|
||||
"translation_composerSubtitle": "Ellenőrzi a zeneszerző fordítási ikon alapértékét.",
|
||||
"translation_targetLanguage": "Célnyelv",
|
||||
"translation_useAppLanguage": "Használja az alkalmazás nyelvének beállítását.",
|
||||
"translation_downloadedModelLabel": "Letöltött modell",
|
||||
"translation_presetModelLabel": "Előre definiált Hugging Face-modell",
|
||||
"translation_manualUrlLabel": "Manuális modell URL",
|
||||
"translation_downloadModel": "Letöltés",
|
||||
"translation_downloading": "Letöltés...",
|
||||
"translation_working": "Munkában vagyok...",
|
||||
"translation_stop": "Halt",
|
||||
"translation_mergingChunks": "A letöltött részek összeállítása a végleges fájlba...",
|
||||
"translation_downloadedModels": "Letöltött modelok",
|
||||
"translation_deleteModel": "Törölje a modellt",
|
||||
"translation_modelDownloaded": "Fordítási modell letöltve.",
|
||||
"translation_downloadStopped": "A letöltés leállt.",
|
||||
"translation_downloadFailed": "Letöltés sikertelen: {error}",
|
||||
"translation_enterUrlFirst": "Addon először egy modell URL-t.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2052,8 +2074,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingHidePin": "PIN elrejtése",
|
||||
"scanner_linuxPairingShowPin": "PIN megjelenítése",
|
||||
"scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot",
|
||||
"scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).",
|
||||
"scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs)."
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "Fordítsa el, mielőtt elküldi",
|
||||
"translation_composerEnabledHint": "A üzenetek fordítását a küldés előtt elvégezzük.",
|
||||
"translation_messageTranslation": "Üzenet fordítása",
|
||||
"translation_composerDisabledHint": "Küldj üzeneteket az eredeti, nyomtatott nyelven.",
|
||||
"translation_translateTo": "Fordítás {language}-ra",
|
||||
"translation_translationOptions": "Fordítási lehetőségek",
|
||||
"translation_systemLanguage": "Rendszer nyelvé",
|
||||
"repeater_cliQuickClockSync": "Óra szinkronizálás",
|
||||
"repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.",
|
||||
"repeater_clockSyncAfterLogin": "Óra szinkronizálás bejelentkezés után",
|
||||
"repeater_guestTools": "Vendégek számára elérhető eszközök",
|
||||
"room_guest": "Szoba szerver információk",
|
||||
"chat_sendMessage": "Üzenet küldése",
|
||||
"repeater_guest": "Adatok a repeaterről",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Többszörös visszaigazolások",
|
||||
"map_sharedAt": "Megosztva",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Kiválasztott akadály",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsHint": "A blokkolt területet megjelölve, hogy a térképen kiemeljük.",
|
||||
"losBlockedSpotsTitle": "Foglalhatatlan területek",
|
||||
"losSelectedObstructionDetails": "Elakadt a {obstruction} miatt, {heightUnit} magasságban, {distanceFromA} méterrel A-tól és {distanceFromB} méterrel B-től ({distanceUnit})."
|
||||
}
|
||||
|
||||
+143
-11
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Modalità privacy abilitata",
|
||||
"settings_privacyModeDisabled": "Modalità privacy disabilitata",
|
||||
"settings_actions": "Azioni",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Invia Annuncio",
|
||||
"settings_sendAdvertisementSubtitle": "Presenza trasmessa ora",
|
||||
"settings_advertisementSent": "Annuncio inviato",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
|
||||
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
|
||||
"contact_teleEnv": "Ambiente di telemetria",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso iniziale del percorso",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
|
||||
"appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
|
||||
@@ -1941,7 +1936,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
|
||||
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2007,6 +2001,34 @@
|
||||
"radioStats_stripWaiting": "Recupero delle statistiche radio…",
|
||||
"radioStats_settingsTile": "Statistiche radio",
|
||||
"radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Tradurre prima di inviare",
|
||||
"translation_enableSubtitle": "Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell'invio.",
|
||||
"translation_enableTitle": "Abilitare la traduzione",
|
||||
"translation_title": "Traduzione",
|
||||
"translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.",
|
||||
"translation_targetLanguage": "Lingua di destinazione",
|
||||
"translation_useAppLanguage": "Utilizza la lingua dell'app",
|
||||
"translation_downloadedModelLabel": "Modello scaricato",
|
||||
"translation_presetModelLabel": "Modello predefinito di Hugging Face",
|
||||
"translation_manualUrlLabel": "URL del modello manuale",
|
||||
"translation_downloadModel": "Scarica il modello",
|
||||
"translation_downloading": "Inizio download...",
|
||||
"translation_working": "Lavoro...",
|
||||
"translation_stop": "Smetta",
|
||||
"translation_downloadedModels": "Modelli scaricati",
|
||||
"translation_mergingChunks": "Unione dei frammenti scaricati in un unico file...",
|
||||
"translation_deleteModel": "Elimina modello",
|
||||
"translation_modelDownloaded": "Modello di traduzione scaricato.",
|
||||
"translation_downloadStopped": "Il download è stato interrotto.",
|
||||
"translation_downloadFailed": "Download fallito: {error}",
|
||||
"translation_enterUrlFirst": "Inserite innanzitutto l'URL del modello.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2036,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Traduzione del messaggio",
|
||||
"translation_translateBeforeSending": "Tradurre prima di inviare",
|
||||
"translation_composerDisabledHint": "Invia messaggi utilizzando la lingua originale, scritta.",
|
||||
"translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.",
|
||||
"translation_translateTo": "Tradurre in {language}",
|
||||
"translation_translationOptions": "Opzioni di traduzione",
|
||||
"translation_systemLanguage": "Lingua del sistema",
|
||||
"scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).",
|
||||
"scanner_linuxPairingShowPin": "Mostra PIN",
|
||||
"scanner_linuxPairingHidePin": "Nascondi PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è)."
|
||||
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth",
|
||||
"scanner_linuxPairingHidePin": "Nascondi il PIN",
|
||||
"repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
|
||||
"repeater_cliQuickDiscovery": "Scopri i Vicini",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Invia automaticamente il comando \"sincronizzazione dell'orologio\" dopo un login riuscito.",
|
||||
"repeater_clockSyncAfterLogin": "Sincronizzazione dell'orologio dopo il login",
|
||||
"repeater_guest": "Informazioni sul ripetitore",
|
||||
"repeater_guestTools": "Strumenti per gli ospiti",
|
||||
"chat_sendMessage": "Invia messaggio",
|
||||
"room_guest": "Informazioni sul server",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "ACK multipli",
|
||||
"map_sharedAt": "Condiviso",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Ostacolo selezionato",
|
||||
"losBlockedSpotsHint": "Tocca un punto bloccato sulla mappa per evidenziarlo.",
|
||||
"losBlockedSpotsTitle": "Posti occupati",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+141
-9
@@ -167,6 +167,8 @@
|
||||
"settings_privacyModeEnabled": "プライバシーモードが有効になっています",
|
||||
"settings_privacyModeDisabled": "プライバシーモードは無効化されています",
|
||||
"settings_actions": "行動",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "広告を送信する",
|
||||
"settings_sendAdvertisementSubtitle": "現在、放送での活動",
|
||||
"settings_advertisementSent": "広告が送信されました",
|
||||
@@ -2012,13 +2014,6 @@
|
||||
"radioStats_stripWaiting": "ラジオの統計情報を取得中…",
|
||||
"radioStats_settingsTile": "ラジオの統計",
|
||||
"radioStats_settingsSubtitle": "ノイズレベル、RSSI、SNR、および通信時間",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "プライバシー設定",
|
||||
"settings_privacySubtitle": "共有する情報の内容を管理する。",
|
||||
"settings_denyAll": "すべてを否定",
|
||||
@@ -2030,7 +2025,6 @@
|
||||
"settings_telemetryEnvironmentMode": "テレメトリ環境モード",
|
||||
"settings_advertLocation": "広告掲載場所",
|
||||
"settings_advertLocationSubtitle": "広告に場所を記載してください。",
|
||||
"settings_multiAck": "複数のACK:{value}",
|
||||
"settings_telemetryModeUpdated": "テレメトリモードが更新されました",
|
||||
"contact_info": "連絡先",
|
||||
"contact_settings": "連絡設定",
|
||||
@@ -2045,6 +2039,34 @@
|
||||
"contact_teleEnvSubtitle": "環境センサーのデータを共有することを許可する",
|
||||
"map_showOverlaps": "リピーターキーの重複",
|
||||
"map_runTraceWithReturnPath": "元の経路に戻る。",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "受信メッセージを翻訳し、送信前に翻訳を適用できるようにする。",
|
||||
"translation_title": "翻訳",
|
||||
"translation_composerTitle": "送信する前に翻訳する",
|
||||
"translation_enableTitle": "翻訳機能を有効にする",
|
||||
"translation_composerSubtitle": "作曲家翻訳アイコンのデフォルト状態を制御する。",
|
||||
"translation_targetLanguage": "翻訳対象言語",
|
||||
"translation_useAppLanguage": "アプリの言語設定",
|
||||
"translation_downloadedModelLabel": "ダウンロードしたモデル",
|
||||
"translation_presetModelLabel": "あらかじめ設定されたHugging Faceモデル",
|
||||
"translation_manualUrlLabel": "マニュアルモデルのURL",
|
||||
"translation_downloadModel": "モデルのダウンロード",
|
||||
"translation_downloading": "ダウンロード中...",
|
||||
"translation_working": "業務中…",
|
||||
"translation_stop": "停止",
|
||||
"translation_mergingChunks": "ダウンロードしたファイルを最終ファイルに結合中...",
|
||||
"translation_downloadedModels": "ダウンロードされたモデル",
|
||||
"translation_deleteModel": "モデルを削除",
|
||||
"translation_modelDownloaded": "翻訳モデルのダウンロードが完了しました。",
|
||||
"translation_downloadStopped": "ダウンロードが中断されました。",
|
||||
"translation_downloadFailed": "ダウンロードに失敗しました:{error}",
|
||||
"translation_enterUrlFirst": "まず、モデルのURLを入力してください。",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2052,8 +2074,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "送信する前に翻訳する",
|
||||
"translation_composerEnabledHint": "メッセージは送信前に翻訳されます。",
|
||||
"translation_messageTranslation": "メッセージの翻訳",
|
||||
"translation_composerDisabledHint": "元のタイプされた言語でメッセージを送信してください。",
|
||||
"translation_translateTo": "{language} への翻訳",
|
||||
"translation_translationOptions": "翻訳の選択肢",
|
||||
"translation_systemLanguage": "システム言語",
|
||||
"scanner_linuxPairingShowPin": "PINを表示",
|
||||
"scanner_linuxPairingHidePin": "PINを非表示",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN",
|
||||
"scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。"
|
||||
"scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。",
|
||||
"repeater_cliQuickClockSync": "クロック同期",
|
||||
"repeater_cliQuickDiscovery": "近隣を発見する",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "ログイン後、時計の時刻を同期する",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。",
|
||||
"room_guest": "ルームサーバーに関する情報",
|
||||
"chat_sendMessage": "メッセージを送信する",
|
||||
"repeater_guest": "繰り返し送信に関する情報",
|
||||
"repeater_guestTools": "ゲスト向けツール",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "複数のACK(応答)",
|
||||
"map_sharedAt": "共有済み",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "地図上で、特定された場所を強調するために、該当する場所をタップしてください。",
|
||||
"losSelectedObstructionTitle": "選択された障害",
|
||||
"losBlockedSpotsTitle": "利用できない場所",
|
||||
"losSelectedObstructionDetails": "{obstruction} によって {heightUnit} の高さで、A地点から {distanceFromA}、B地点から {distanceFromB} ({distanceUnit}) の距離で塞がれています。",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
|
||||
}
|
||||
|
||||
+142
-10
@@ -167,6 +167,8 @@
|
||||
"settings_privacyModeEnabled": "개인 정보 보호 모드 활성화",
|
||||
"settings_privacyModeDisabled": "개인 정보 보호 모드 비활성화",
|
||||
"settings_actions": "행동",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "광고 전송",
|
||||
"settings_sendAdvertisementSubtitle": "방송 활동",
|
||||
"settings_advertisementSent": "광고 전송",
|
||||
@@ -2012,13 +2014,6 @@
|
||||
"radioStats_stripWaiting": "라디오 통계 가져오기…",
|
||||
"radioStats_settingsTile": "라디오 통계",
|
||||
"radioStats_settingsSubtitle": "잡음 수준, RSSI, 신호 대 잡음비, 통신 시간",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "개인 정보 설정",
|
||||
"settings_privacySubtitle": "어떤 정보를 공유할지 통제하세요.",
|
||||
"settings_privacySettingsDescription": "어떤 정보를 기기가 다른 사람들과 공유할지 선택하세요.",
|
||||
@@ -2030,7 +2025,6 @@
|
||||
"settings_telemetryEnvironmentMode": "텔레메트리 환경 모드",
|
||||
"settings_advertLocation": "광고 위치",
|
||||
"settings_advertLocationSubtitle": "광고에 위치 정보를 포함하세요.",
|
||||
"settings_multiAck": "다중 ACK: {value}",
|
||||
"settings_telemetryModeUpdated": "텔레메트리 모드 업데이트 완료",
|
||||
"contact_info": "연락처",
|
||||
"contact_settings": "연락처 설정",
|
||||
@@ -2045,6 +2039,34 @@
|
||||
"contact_teleEnvSubtitle": "환경 센서 데이터를 공유하도록 허용",
|
||||
"map_showOverlaps": "반복 키 중복",
|
||||
"map_runTraceWithReturnPath": "원래 경로로 돌아가세요.",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "입력 메시지를 번역하고, 미리 번역 기능을 제공합니다.",
|
||||
"translation_title": "번역",
|
||||
"translation_enableTitle": "번역 기능 활성화",
|
||||
"translation_composerTitle": "보내기 전에 번역",
|
||||
"translation_composerSubtitle": "컴포저 번역 아이콘의 기본 상태를 제어합니다.",
|
||||
"translation_targetLanguage": "목표 언어",
|
||||
"translation_useAppLanguage": "앱 언어 사용",
|
||||
"translation_downloadedModelLabel": "다운로드한 모델",
|
||||
"translation_presetModelLabel": "사전에 설정된 Hugging Face 모델",
|
||||
"translation_manualUrlLabel": "수동 모델 URL",
|
||||
"translation_downloadModel": "모델 다운로드",
|
||||
"translation_downloading": "다운로드 중...",
|
||||
"translation_working": "업무 중...",
|
||||
"translation_stop": "멈춰",
|
||||
"translation_mergingChunks": "다운로드한 파일 조각들을 최종 파일로 병합 중...",
|
||||
"translation_downloadedModels": "다운로드한 모델",
|
||||
"translation_deleteModel": "모델 삭제",
|
||||
"translation_modelDownloaded": "번역 모델이 다운로드되었습니다.",
|
||||
"translation_downloadStopped": "다운로드 중단됨.",
|
||||
"translation_downloadFailed": "다운로드 실패: {error}",
|
||||
"translation_enterUrlFirst": "먼저 모델 URL을 입력하세요.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2052,8 +2074,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingShowPin": "PIN 표시",
|
||||
"scanner_linuxPairingPinTitle": "블루투스 페어링 PIN",
|
||||
"scanner_linuxPairingHidePin": "PIN 숨기기",
|
||||
"scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요)."
|
||||
"scanner_linuxPairingShowPin": "PIN 보기",
|
||||
"scanner_linuxPairingPinPrompt": "{deviceName}의 PIN을 입력하세요 (해당하는 경우에만 입력).",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerEnabledHint": "메시지는 전송하기 전에 번역될 것입니다.",
|
||||
"translation_translateBeforeSending": "보내기 전에 번역",
|
||||
"translation_messageTranslation": "메시지 번역",
|
||||
"translation_composerDisabledHint": "원래 작성된 언어로 메시지를 보내세요.",
|
||||
"translation_translateTo": "{language} 번역",
|
||||
"translation_translationOptions": "번역 옵션",
|
||||
"translation_systemLanguage": "시스템 언어",
|
||||
"repeater_cliQuickClockSync": "시계 동기화",
|
||||
"repeater_cliQuickDiscovery": "이웃 발견하기",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "로그인 후 시계 동기화",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.",
|
||||
"repeater_guestTools": "손님용 도구",
|
||||
"chat_sendMessage": "메시지를 보내기",
|
||||
"repeater_guest": "반복 장비 정보",
|
||||
"room_guest": "서버 정보",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "다중 ACK",
|
||||
"map_sharedAt": "공유됨",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.",
|
||||
"losBlockedSpotsTitle": "차단된 공간",
|
||||
"losSelectedObstructionTitle": "선택된 장애물",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+417
-16
@@ -202,6 +202,12 @@ abstract class AppLocalizations {
|
||||
/// **'Close'**
|
||||
String get common_close;
|
||||
|
||||
/// No description provided for @common_done.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Done'**
|
||||
String get common_done;
|
||||
|
||||
/// No description provided for @common_edit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -901,8 +907,8 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @settings_multiAck.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Multi-ACKs: {value}'**
|
||||
String settings_multiAck(String value);
|
||||
/// **'Multi-ACKs'**
|
||||
String get settings_multiAck;
|
||||
|
||||
/// No description provided for @settings_telemetryModeUpdated.
|
||||
///
|
||||
@@ -916,6 +922,18 @@ abstract class AppLocalizations {
|
||||
/// **'Actions'**
|
||||
String get settings_actions;
|
||||
|
||||
/// No description provided for @settings_deleteAllPaths.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete All Paths'**
|
||||
String get settings_deleteAllPaths;
|
||||
|
||||
/// No description provided for @settings_deleteAllPathsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear all path data from contacts.'**
|
||||
String get settings_deleteAllPathsSubtitle;
|
||||
|
||||
/// No description provided for @settings_sendAdvertisement.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -988,17 +1006,17 @@ abstract class AppLocalizations {
|
||||
/// **'Debug'**
|
||||
String get settings_debug;
|
||||
|
||||
/// No description provided for @settings_bleDebugLog.
|
||||
/// No description provided for @settings_companionDebugLog.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'BLE Debug Log'**
|
||||
String get settings_bleDebugLog;
|
||||
/// **'Companion Debug Log'**
|
||||
String get settings_companionDebugLog;
|
||||
|
||||
/// No description provided for @settings_bleDebugLogSubtitle.
|
||||
/// No description provided for @settings_companionDebugLogSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'BLE commands, responses, and raw data'**
|
||||
String get settings_bleDebugLogSubtitle;
|
||||
/// **'BLE/TCP/USB commands, responses, and raw data'**
|
||||
String get settings_companionDebugLogSubtitle;
|
||||
|
||||
/// No description provided for @settings_appDebugLog.
|
||||
///
|
||||
@@ -2044,6 +2062,12 @@ abstract class AppLocalizations {
|
||||
/// **'Public'**
|
||||
String get channels_public;
|
||||
|
||||
/// No description provided for @channels_via.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'via {path}'**
|
||||
String channels_via(String path);
|
||||
|
||||
/// No description provided for @channels_private.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2296,6 +2320,18 @@ abstract class AppLocalizations {
|
||||
/// **'No messages yet'**
|
||||
String get chat_noMessages;
|
||||
|
||||
/// No description provided for @chat_sendMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send message'**
|
||||
String get chat_sendMessage;
|
||||
|
||||
/// No description provided for @chat_sendMessageTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send a message to {contactName}'**
|
||||
String chat_sendMessageTo(String contactName);
|
||||
|
||||
/// No description provided for @chat_sendMessageToStart.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2326,12 +2362,6 @@ abstract class AppLocalizations {
|
||||
/// **'Location'**
|
||||
String get chat_location;
|
||||
|
||||
/// No description provided for @chat_sendMessageTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send a message to {contactName}'**
|
||||
String chat_sendMessageTo(String contactName);
|
||||
|
||||
/// No description provided for @chat_typeMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2656,6 +2686,12 @@ abstract class AppLocalizations {
|
||||
/// **'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:
|
||||
@@ -2818,6 +2854,18 @@ abstract class AppLocalizations {
|
||||
/// **'Unread: {count}'**
|
||||
String chat_unread(int count);
|
||||
|
||||
/// No description provided for @chat_markAsUnread.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mark as Unread'**
|
||||
String get chat_markAsUnread;
|
||||
|
||||
/// No description provided for @chat_newMessages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New messages'**
|
||||
String get chat_newMessages;
|
||||
|
||||
/// No description provided for @chat_openLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2962,6 +3010,42 @@ abstract class AppLocalizations {
|
||||
/// **'Flags'**
|
||||
String get map_flags;
|
||||
|
||||
/// No description provided for @map_type.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Type'**
|
||||
String get map_type;
|
||||
|
||||
/// No description provided for @map_path.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Path'**
|
||||
String get map_path;
|
||||
|
||||
/// No description provided for @map_location.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Location'**
|
||||
String get map_location;
|
||||
|
||||
/// No description provided for @map_estLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Est. Location'**
|
||||
String get map_estLocation;
|
||||
|
||||
/// No description provided for @map_publicKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Public Key'**
|
||||
String get map_publicKey;
|
||||
|
||||
/// No description provided for @map_publicKeyPrefixHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'e.g. ab12'**
|
||||
String get map_publicKeyPrefixHint;
|
||||
|
||||
/// No description provided for @map_shareMarkerHere.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3124,6 +3208,12 @@ abstract class AppLocalizations {
|
||||
/// **'Shared pin'**
|
||||
String get map_sharedPin;
|
||||
|
||||
/// No description provided for @map_sharedAt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Shared'**
|
||||
String get map_sharedAt;
|
||||
|
||||
/// No description provided for @map_joinRoom.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3432,13 +3522,13 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @login_repeaterDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter the repeater password to access settings and status.'**
|
||||
/// **'Enter the repeater password for guest or admin access.'**
|
||||
String get login_repeaterDescription;
|
||||
|
||||
/// No description provided for @login_roomDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter the room password to access settings and status.'**
|
||||
/// **'Enter the room password for guest or admin access.'**
|
||||
String get login_roomDescription;
|
||||
|
||||
/// No description provided for @login_routing.
|
||||
@@ -3603,12 +3693,30 @@ abstract class AppLocalizations {
|
||||
/// **'Room Server Management'**
|
||||
String get room_management;
|
||||
|
||||
/// No description provided for @repeater_guest.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Repeater Information'**
|
||||
String get repeater_guest;
|
||||
|
||||
/// No description provided for @room_guest.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Room Server Information'**
|
||||
String get room_guest;
|
||||
|
||||
/// No description provided for @repeater_managementTools.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Management Tools'**
|
||||
String get repeater_managementTools;
|
||||
|
||||
/// No description provided for @repeater_guestTools.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Guest Tools'**
|
||||
String get repeater_guestTools;
|
||||
|
||||
/// No description provided for @repeater_status.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3669,6 +3777,18 @@ abstract class AppLocalizations {
|
||||
/// **'Configure repeater parameters'**
|
||||
String get repeater_settingsSubtitle;
|
||||
|
||||
/// Repeater setting: auto sync device clock after successful login
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clock sync after login'**
|
||||
String get repeater_clockSyncAfterLogin;
|
||||
|
||||
/// Repeater setting subtitle: describes the clock sync after login behavior
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Automatically send \"clock sync\" after a successful login'**
|
||||
String get repeater_clockSyncAfterLoginSubtitle;
|
||||
|
||||
/// No description provided for @repeater_statusTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -4322,6 +4442,18 @@ abstract class AppLocalizations {
|
||||
/// **'Clock'**
|
||||
String get repeater_cliQuickClock;
|
||||
|
||||
/// No description provided for @repeater_cliQuickClockSync.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clock Sync'**
|
||||
String get repeater_cliQuickClockSync;
|
||||
|
||||
/// No description provided for @repeater_cliQuickDiscovery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Discover Neighbors'**
|
||||
String get repeater_cliQuickDiscovery;
|
||||
|
||||
/// No description provided for @repeater_cliHelpAdvert.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -5592,6 +5724,47 @@ abstract class AppLocalizations {
|
||||
/// **'Terrain'**
|
||||
String get losLegendTerrain;
|
||||
|
||||
/// No description provided for @losBlockedSpotsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Blocked spots'**
|
||||
String get losBlockedSpotsTitle;
|
||||
|
||||
/// No description provided for @losBlockedSpotsHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap a blocked spot to highlight it on the map.'**
|
||||
String get losBlockedSpotsHint;
|
||||
|
||||
/// No description provided for @losBlockedSpotChip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{distance} {distanceUnit} • {obstruction} {heightUnit}'**
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
);
|
||||
|
||||
/// No description provided for @losSelectedObstructionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Selected obstruction'**
|
||||
String get losSelectedObstructionTitle;
|
||||
|
||||
/// No description provided for @losSelectedObstructionDetails.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).'**
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
);
|
||||
|
||||
/// No description provided for @losFrequencyLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -6149,6 +6322,132 @@ abstract class AppLocalizations {
|
||||
/// **'Noise floor, RSSI, SNR, and airtime'**
|
||||
String get radioStats_settingsSubtitle;
|
||||
|
||||
/// No description provided for @translation_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translation'**
|
||||
String get translation_title;
|
||||
|
||||
/// No description provided for @translation_enableTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable translation'**
|
||||
String get translation_enableTitle;
|
||||
|
||||
/// No description provided for @translation_enableSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate incoming messages and allow pre-send translation.'**
|
||||
String get translation_enableSubtitle;
|
||||
|
||||
/// No description provided for @translation_composerTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate before sending'**
|
||||
String get translation_composerTitle;
|
||||
|
||||
/// No description provided for @translation_composerSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Controls the default state of the composer translation icon.'**
|
||||
String get translation_composerSubtitle;
|
||||
|
||||
/// No description provided for @translation_targetLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Target language'**
|
||||
String get translation_targetLanguage;
|
||||
|
||||
/// No description provided for @translation_useAppLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use app language'**
|
||||
String get translation_useAppLanguage;
|
||||
|
||||
/// No description provided for @translation_downloadedModelLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloaded model'**
|
||||
String get translation_downloadedModelLabel;
|
||||
|
||||
/// No description provided for @translation_presetModelLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Preset Hugging Face model'**
|
||||
String get translation_presetModelLabel;
|
||||
|
||||
/// No description provided for @translation_manualUrlLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manual model URL'**
|
||||
String get translation_manualUrlLabel;
|
||||
|
||||
/// No description provided for @translation_downloadModel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download model'**
|
||||
String get translation_downloadModel;
|
||||
|
||||
/// No description provided for @translation_downloading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloading...'**
|
||||
String get translation_downloading;
|
||||
|
||||
/// No description provided for @translation_working.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Working...'**
|
||||
String get translation_working;
|
||||
|
||||
/// No description provided for @translation_stop.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stop'**
|
||||
String get translation_stop;
|
||||
|
||||
/// No description provided for @translation_mergingChunks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Merging downloaded chunks into final file...'**
|
||||
String get translation_mergingChunks;
|
||||
|
||||
/// No description provided for @translation_downloadedModels.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloaded models'**
|
||||
String get translation_downloadedModels;
|
||||
|
||||
/// No description provided for @translation_deleteModel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete model'**
|
||||
String get translation_deleteModel;
|
||||
|
||||
/// No description provided for @translation_modelDownloaded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translation model downloaded.'**
|
||||
String get translation_modelDownloaded;
|
||||
|
||||
/// No description provided for @translation_downloadStopped.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download stopped.'**
|
||||
String get translation_downloadStopped;
|
||||
|
||||
/// No description provided for @translation_downloadFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download failed: {error}'**
|
||||
String translation_downloadFailed(String error);
|
||||
|
||||
/// No description provided for @translation_enterUrlFirst.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter a model URL first.'**
|
||||
String get translation_enterUrlFirst;
|
||||
|
||||
/// No description provided for @scanner_linuxPairingShowPin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -6172,6 +6471,108 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter PIN for {deviceName} (leave blank if none).'**
|
||||
String scanner_linuxPairingPinPrompt(String deviceName);
|
||||
|
||||
/// No description provided for @translation_messageTranslation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Message translation'**
|
||||
String get translation_messageTranslation;
|
||||
|
||||
/// No description provided for @translation_translateBeforeSending.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate before sending'**
|
||||
String get translation_translateBeforeSending;
|
||||
|
||||
/// No description provided for @translation_composerEnabledHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Messages will be translated before send.'**
|
||||
String get translation_composerEnabledHint;
|
||||
|
||||
/// No description provided for @translation_composerDisabledHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send messages in the original typed language.'**
|
||||
String get translation_composerDisabledHint;
|
||||
|
||||
/// No description provided for @translation_translateTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translate to {language}'**
|
||||
String translation_translateTo(String language);
|
||||
|
||||
/// No description provided for @translation_translationOptions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Translation options'**
|
||||
String get translation_translationOptions;
|
||||
|
||||
/// No description provided for @translation_systemLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'System language'**
|
||||
String get translation_systemLanguage;
|
||||
|
||||
/// No description provided for @background_serviceTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'MeshCore running'**
|
||||
String get background_serviceTitle;
|
||||
|
||||
/// No description provided for @background_serviceText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Keeping BLE connected'**
|
||||
String get background_serviceText;
|
||||
|
||||
/// No description provided for @appSettings_translationModelDeleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Deleted {name}'**
|
||||
String appSettings_translationModelDeleted(String name);
|
||||
|
||||
/// No description provided for @appSettings_translationModelDeleteFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to delete: {error}'**
|
||||
String appSettings_translationModelDeleteFailed(String error);
|
||||
|
||||
/// No description provided for @channels_channelUpdateFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to update channel: {error}'**
|
||||
String channels_channelUpdateFailed(String error);
|
||||
|
||||
/// No description provided for @contact_typeChat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Chat'**
|
||||
String get contact_typeChat;
|
||||
|
||||
/// No description provided for @contact_typeRepeater.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Repeater'**
|
||||
String get contact_typeRepeater;
|
||||
|
||||
/// No description provided for @contact_typeRoom.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Room'**
|
||||
String get contact_typeRoom;
|
||||
|
||||
/// No description provided for @contact_typeSensor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sensor'**
|
||||
String get contact_typeSensor;
|
||||
|
||||
/// No description provided for @contact_typeUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unknown'**
|
||||
String get contact_typeUnknown;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Затвори';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Редактирай';
|
||||
|
||||
@@ -437,9 +440,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
'Включи местоположение в обявата';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мулти-потвърди: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Множество потвърждения';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
|
||||
@@ -447,6 +448,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Изпрати Реклама';
|
||||
|
||||
@@ -488,11 +496,11 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_debug => 'Отстрани';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Лог за отстраняване на грешки на BLE';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Команди, отговори и сурови данни BLE';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog =>
|
||||
@@ -1097,6 +1105,11 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Публично';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Личен';
|
||||
|
||||
@@ -1239,6 +1252,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Няма съобщения.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Изпратете съобщение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Изпрати съобщение на $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.';
|
||||
|
||||
@@ -1258,11 +1279,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Местоположение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Изпрати съобщение на $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Въведете съобщение...';
|
||||
|
||||
@@ -1455,6 +1471,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Успехи';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Премахни пътя';
|
||||
|
||||
@@ -1557,6 +1576,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Непрочетени: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Отваряне на връзката?';
|
||||
|
||||
@@ -1638,6 +1663,24 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Флаг';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Споделете маркер тук';
|
||||
|
||||
@@ -1723,6 +1766,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Споделено копие';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Споделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присъедини се към стаята';
|
||||
|
||||
@@ -2016,9 +2062,18 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Управление на сървъра за стая';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Информация за ретранслаторите';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Информация за сървъра на стаята';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Инструменти за управление';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Инструменти за гости';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Статус';
|
||||
|
||||
@@ -2053,6 +2108,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Конфигурирайте параметрите на репитера';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Синхронизиране на часовника след влизане';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Статус на повтарянето';
|
||||
|
||||
@@ -2429,6 +2492,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Часовник';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Синхронизация на часовника';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Открий Съседи';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет';
|
||||
|
||||
@@ -3212,6 +3281,37 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Терен';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Ограничени места';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Кликнете върху блокираната точка, за да я отбележите на картата.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Избрано препятствие';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Честота';
|
||||
|
||||
@@ -3568,18 +3668,148 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Ниво на шума, RSSI, SNR и време на пренос';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Превод';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Активирайте превода';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Преведете преди да изпратите';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Контролира началния статус на иконата за превод, създадена от композитора.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Целеви език';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Използвайте езика на приложението';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Изтегнат модел';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Предварително конфигуриран модел от Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL на ръководството';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Изтеглете модела';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Изтегляне...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Работа...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Спрете';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Съединяване на изтеглените части в един файл...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Изтеглени модели';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Изтриване на модела';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Моделът за превод е изтеглен.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Изтеглянето беше прекъснато.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Не успях да изтегля: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Въведете първо URL адрес на модела.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Покажи PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Скрий ПИН';
|
||||
String get scanner_linuxPairingHidePin => 'Скриване на PIN кода';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle =>
|
||||
'PIN код за сдвояване на Bluetooth';
|
||||
String get scanner_linuxPairingPinTitle => 'PIN за съвпадение чрез Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Въведете ПИН за $deviceName (оставете празно, ако няма).';
|
||||
return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Превод на съобщението';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Преведете преди да изпратите';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Съобщенията ще бъдат преведени, преди да бъдат изпратени.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Изпращайте съобщения на оригиналния въведен език.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Превеждане на $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Опции за превод';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Език на системата';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Schließen';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Bearbeiten';
|
||||
|
||||
@@ -435,9 +438,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Ort in der Anzeige einbeziehen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Mehrfach-Bestätigungen: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Mehrere Bestätigungen';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
|
||||
@@ -445,6 +446,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Aktionen';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Sende Ankündigung';
|
||||
|
||||
@@ -485,11 +493,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get settings_debug => 'Fehlerbehebung';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE-Debug-Protokoll';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'BLE-Befehle, Antworten und Rohdaten';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'App-Debug-Protokoll';
|
||||
@@ -1092,6 +1100,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Öffentlich';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privat';
|
||||
|
||||
@@ -1238,6 +1251,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Noch keine Nachrichten.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Nachricht senden';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Sende eine Nachricht an $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.';
|
||||
|
||||
@@ -1257,11 +1278,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Ort';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Sende eine Nachricht an $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Eine Nachricht eingeben...';
|
||||
|
||||
@@ -1454,6 +1470,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Erfolgreich';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Pfad entfernen';
|
||||
|
||||
@@ -1554,6 +1573,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Ungelesen: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Link öffnen?';
|
||||
|
||||
@@ -1635,6 +1660,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flags';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
|
||||
|
||||
@@ -1720,6 +1763,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Gemeinsames Passwort';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Geteilt';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Beitreten Sie dem Raum';
|
||||
|
||||
@@ -2014,9 +2060,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Raum-Server-Verwaltung';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informationen zu Repeatern';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informationen zum Room Server';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Verwaltungs-Tools';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Gastwerkzeuge';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -2049,6 +2104,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Uhrzeit-Synchronisation nach dem Anmelden';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Repeaterstatus';
|
||||
|
||||
@@ -2429,6 +2492,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Uhr';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Uhr Synchronisieren';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung';
|
||||
|
||||
@@ -3217,6 +3286,37 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Gelände';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Reservierte Plätze';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance • $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Ausgewählte Behinderung';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blockiert durch $obstruction in einer Höhe von $heightUnit, $distanceFromA von A und $distanceFromB von B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequenz';
|
||||
|
||||
@@ -3577,6 +3677,77 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Übersetzung';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivieren Sie die Übersetzung';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Übersetzen Sie vor dem Versenden';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Zielsprache';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Verwenden Sie die App-Sprache';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Heruntergeladenes Modell';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Vordefinierter Hugging Face-Modell';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL für das manuelle Modell';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Modell herunterladen';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Herunterladen...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Arbeiten...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stopp';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Zusammenführen der heruntergeladenen Teile in die finale Datei...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Heruntergeladene Modelle';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Modell löschen';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded =>
|
||||
'Übersetzungsmotor heruntergeladen.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Herunterladen abgebrochen.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Geben Sie zunächst die URL eines Modells ein.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'PIN anzeigen';
|
||||
|
||||
@@ -3590,4 +3761,66 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Geben Sie die PIN für $deviceName ein (leer lassen, falls keine).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Nachricht übersetzen';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Übersetzen Sie vor dem Versenden';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Die Nachrichten werden vor dem Versenden übersetzt.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Nachrichten in der ursprünglichen, getippten Sprache senden.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Übersetzen Sie auf $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Übersetzungsmöglichkeiten';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Sprache des Systems';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Close';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Edit';
|
||||
|
||||
@@ -427,9 +430,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Include location in advert.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
|
||||
@@ -437,6 +438,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Send Advertisement';
|
||||
|
||||
@@ -476,11 +484,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_debug => 'Debug';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE Debug Log';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'BLE commands, responses, and raw data';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'App Debug Log';
|
||||
@@ -1074,6 +1082,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Public';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Private';
|
||||
|
||||
@@ -1213,6 +1226,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Send message';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Send a message to $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Send a message to get started';
|
||||
|
||||
@@ -1232,11 +1253,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Location';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Send a message to $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Type a message...';
|
||||
|
||||
@@ -1427,6 +1443,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'successes';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Remove path';
|
||||
|
||||
@@ -1524,6 +1543,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Unread: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Open Link?';
|
||||
|
||||
@@ -1605,6 +1630,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flags';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Share marker here';
|
||||
|
||||
@@ -1689,6 +1732,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Shared pin';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Shared';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Join Room';
|
||||
|
||||
@@ -1868,11 +1914,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get login_repeaterDescription =>
|
||||
'Enter the repeater password to access settings and status.';
|
||||
'Enter the repeater password for guest or admin access.';
|
||||
|
||||
@override
|
||||
String get login_roomDescription =>
|
||||
'Enter the room password to access settings and status.';
|
||||
'Enter the room password for guest or admin access.';
|
||||
|
||||
@override
|
||||
String get login_routing => 'Routing';
|
||||
@@ -1976,9 +2022,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Room Server Management';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Repeater Information';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Room Server Information';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Management Tools';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Guest Tools';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -2011,6 +2066,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configure repeater parameters';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => 'Clock sync after login';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatically send \"clock sync\" after a successful login';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Repeater Status';
|
||||
|
||||
@@ -2379,6 +2441,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Clock';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Clock Sync';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Discover Neighbors';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Sends an advertisement packet';
|
||||
|
||||
@@ -3157,6 +3225,37 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrain';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Blocked spots';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Tap a blocked spot to highlight it on the map.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Selected obstruction';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequency';
|
||||
|
||||
@@ -3502,6 +3601,74 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Noise floor, RSSI, SNR, and airtime';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Translation';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Enable translation';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Translate incoming messages and allow pre-send translation.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Translate before sending';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controls the default state of the composer translation icon.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Target language';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Use app language';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Downloaded model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => 'Preset Hugging Face model';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Manual model URL';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Download model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Downloading...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Working...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stop';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Merging downloaded chunks into final file...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Downloaded models';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Delete model';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Translation model downloaded.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Download stopped.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Enter a model URL first.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Show PIN';
|
||||
|
||||
@@ -3515,4 +3682,65 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Enter PIN for $deviceName (leave blank if none).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Message translation';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Translate before sending';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Messages will be translated before send.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Send messages in the original typed language.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Translate to $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Translation options';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'System language';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Cerrar';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Editar';
|
||||
|
||||
@@ -434,9 +437,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Múltiples respuestas de confirmación';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
|
||||
@@ -444,6 +445,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Acciones';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Enviar Anuncio';
|
||||
|
||||
@@ -486,11 +494,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get settings_debug => 'Depurar';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Registro de Depuración BLE';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Comandos, respuestas y datos brutos de BLE';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Registro de Depuración de la App';
|
||||
@@ -1094,6 +1102,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Público';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privado';
|
||||
|
||||
@@ -1238,6 +1251,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Aún no hay mensajes';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Enviar mensaje';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar un mensaje a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar';
|
||||
|
||||
@@ -1257,11 +1278,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Ubicación';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar un mensaje a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Escribe un mensaje...';
|
||||
|
||||
@@ -1452,6 +1468,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Éxitos';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Eliminar ruta';
|
||||
|
||||
@@ -1553,6 +1572,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Sin leer: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => '¿Abrir enlace?';
|
||||
|
||||
@@ -1634,6 +1659,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Banderas';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Compartir marcador aquí';
|
||||
|
||||
@@ -1719,6 +1762,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Pin compartido';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Compartido';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Únete a la sala';
|
||||
|
||||
@@ -2012,9 +2058,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Administración del Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Información sobre repetidores';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Información del servidor';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Herramientas de Gestión';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Herramientas para invitados';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Estado';
|
||||
|
||||
@@ -2047,6 +2102,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Sincronización del reloj después de iniciar sesión';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Estado del Repetidor';
|
||||
|
||||
@@ -2423,6 +2486,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Reloj';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Sincronización del reloj';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Descubrir Vecinos';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad';
|
||||
|
||||
@@ -3211,6 +3280,37 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Espacios ocupados';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Seleccione un punto bloqueado para resaltarlo en el mapa.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Obstrucción seleccionada';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Bloqueado por $obstruction a una altura de $heightUnit, a $distanceFromA metros de A y a $distanceFromB metros de B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frecuencia';
|
||||
|
||||
@@ -3571,16 +3671,149 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
'Nivel de ruido, RSSI, SNR y tiempo de transmisión';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
|
||||
String get translation_title => 'Traducción';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Habilitar la traducción';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Traducir los mensajes entrantes y permitir la traducción previa al envío.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Traducir antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controla el estado predeterminado del icono de traducción del compositor.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Idioma de destino';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage =>
|
||||
'Utilizar el idioma de la aplicación';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modelo descargado';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Modelo predefinido de Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL del modelo manual';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Descargar el modelo';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Descargando...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Trabajando...';
|
||||
|
||||
@override
|
||||
String get translation_stop => '¡Detente!';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Combinando los fragmentos descargados en el archivo final...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modelos descargados';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Eliminar modelo';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modelo de traducción descargado.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'La descarga se ha detenido.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'No se pudo descargar: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Primero, introduzca la URL del modelo.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Mostrar código PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'PIN de emparejamiento Bluetooth';
|
||||
String get scanner_linuxPairingPinTitle =>
|
||||
'PIN para emparejar dispositivos Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Introduzca el PIN para $deviceName (déjelo en blanco si no hay ninguno).';
|
||||
return 'Introduzca el código PIN para $deviceName (deje en blanco si no hay ninguno).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Traducción del mensaje';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Traducir antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Los mensajes serán traducidos antes de ser enviados.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Envía mensajes utilizando el lenguaje escrito original.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Traducir a $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opciones de traducción';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Idioma del sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Fermer';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Modifier';
|
||||
|
||||
@@ -438,9 +441,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Inclure l\'emplacement dans l\'annonce';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs : $value';
|
||||
}
|
||||
String get settings_multiAck => 'Plusieurs accusés de réception';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
@@ -449,6 +450,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'S\'annoncer';
|
||||
|
||||
@@ -490,11 +498,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_debug => 'Déboguer';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Journal de débogage BLE';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Commandes BLE, réponses et données brutes';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Journal de débogage de l\'application';
|
||||
@@ -559,10 +567,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_bandwidth => 'Bande passante';
|
||||
|
||||
@override
|
||||
String get settings_spreadingFactor => 'Facteur de répartition';
|
||||
String get settings_spreadingFactor => 'Facteur de répartition (SF)';
|
||||
|
||||
@override
|
||||
String get settings_codingRate => 'Taux de codage';
|
||||
String get settings_codingRate => 'Taux de codage (CR)';
|
||||
|
||||
@override
|
||||
String get settings_txPower => 'TX Puissance (dBm)';
|
||||
@@ -946,7 +954,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Rechercher $number$str serveurs de salle...';
|
||||
return 'Rechercher $number$str room server...';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1099,6 +1107,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Public';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privé';
|
||||
|
||||
@@ -1229,7 +1242,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'N\'importe qui peut rejoindre les canaux #hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scanner un code QR';
|
||||
String get channels_scanQrCode => 'Scanner un QR code';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
|
||||
@@ -1243,6 +1256,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Aucun message pour le moment.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Envoyer un message';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Envoyer un message à $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Envoyer un message pour commencer';
|
||||
|
||||
@@ -1262,11 +1283,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Emplacement';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Envoyer un message à $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Saisir un message...';
|
||||
|
||||
@@ -1459,6 +1475,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Succès';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Supprimer le chemin';
|
||||
|
||||
@@ -1489,7 +1508,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Utiliser le commutateur de routage dans la barre d\'application';
|
||||
'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 =>
|
||||
@@ -1562,6 +1581,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Non lu : $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Ouvrir le lien ?';
|
||||
|
||||
@@ -1614,7 +1639,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get map_repeater => 'Répéteur';
|
||||
|
||||
@override
|
||||
String get map_room => 'Salle';
|
||||
String get map_room => 'Room Server';
|
||||
|
||||
@override
|
||||
String get map_sensor => 'Capteur';
|
||||
@@ -1644,6 +1669,24 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Drapeaux';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Partager le marqueur ici';
|
||||
|
||||
@@ -1730,7 +1773,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get map_sharedPin => 'Clé partagée';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Rejoindre la salle';
|
||||
String get map_sharedAt => 'Partagé';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Rejoindre le room server';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'Gérer le répéteur';
|
||||
@@ -1999,7 +2045,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Aucun répéteur ou serveur de salle n\'a été trouvé.';
|
||||
'Aucun répéteur ou room server n\'a été trouvé.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
@@ -2023,9 +2069,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Administrattion Room Server';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informations sur les répéteurs';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informations sur le serveur';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Outils de Gestion';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Outils pour les invités';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'État';
|
||||
|
||||
@@ -2059,6 +2114,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Configurer les paramètres du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Synchronisation de l\'horloge après la connexion';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Envoyer automatiquement une notification \"synchronisation de l\'heure\" après une connexion réussie.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'État du répéteur';
|
||||
|
||||
@@ -2209,10 +2272,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_bandwidth => 'Bande passante';
|
||||
|
||||
@override
|
||||
String get repeater_spreadingFactor => 'Facteur de répartition';
|
||||
String get repeater_spreadingFactor => 'Facteur de répartition (SF)';
|
||||
|
||||
@override
|
||||
String get repeater_codingRate => 'Taux de codage';
|
||||
String get repeater_codingRate => 'Taux de codage (CR)';
|
||||
|
||||
@override
|
||||
String get repeater_locationSettings => 'Paramètres de localisation';
|
||||
@@ -2235,7 +2298,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_features => 'Fonctionnalités';
|
||||
|
||||
@override
|
||||
String get repeater_packetForwarding => 'Transfert de paquets';
|
||||
String get repeater_packetForwarding => 'Mode répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_packetForwardingSubtitle =>
|
||||
@@ -2442,6 +2505,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Horloge';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Découvrir les voisins';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce';
|
||||
|
||||
@@ -2896,11 +2965,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_scanQr => 'Scanner la communauté QR';
|
||||
String get community_scanQr => 'Scanner un QR code de communauté';
|
||||
|
||||
@override
|
||||
String get community_scanInstructions =>
|
||||
'Pointez l\'appareil photo vers un code QR communautaire.';
|
||||
'Pointez l\'appareil photo vers un QR code de communauté.';
|
||||
|
||||
@override
|
||||
String get community_showQr => 'Afficher le QR Code';
|
||||
@@ -2940,7 +3009,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
|
||||
|
||||
@override
|
||||
String get community_invalidQrCode => 'Code QR de communauté non valide';
|
||||
String get community_invalidQrCode => 'QR code de communauté non valide';
|
||||
|
||||
@override
|
||||
String get community_alreadyMember => 'Déjà membre';
|
||||
@@ -2964,7 +3033,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get community_scanOrCreate =>
|
||||
'Scanner un code QR ou créer une communauté pour commencer';
|
||||
'Scanner un QR code ou créer une communauté pour commencer';
|
||||
|
||||
@override
|
||||
String get community_manageCommunities => 'Gérer les Communautés';
|
||||
@@ -2992,7 +3061,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
|
||||
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3013,7 +3082,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
|
||||
return 'Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3229,6 +3298,37 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrain';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Places occupés';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Obstruction sélectionnée';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Bloqué par $obstruction, à une hauteur de $heightUnit, à une distance de $distanceFromA par rapport à A et à une distance de $distanceFromB par rapport à B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Fréquence';
|
||||
|
||||
@@ -3261,11 +3361,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get contacts_repeaterPing => 'Pinguer le répéteur';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace =>
|
||||
'Traçage du chemin vers le serveur de la salle';
|
||||
String get contacts_roomPathTrace => 'Traçage du chemin vers le room server';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Pinguer le serveur de la salle';
|
||||
String get contacts_roomPing => 'Pinguer le room server';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Tracer le chemin';
|
||||
@@ -3371,7 +3470,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exporter les répéteurs / serveur de salle au format GPX';
|
||||
'Exporter les répéteurs / room servers au format GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
@@ -3409,7 +3508,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Emplacements des serveurs de répéteur et de salle';
|
||||
'Emplacements des répéteurs et room servers';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Emplacements des compagnons';
|
||||
@@ -3460,11 +3559,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Ajouter automatiquement les serveurs de salle';
|
||||
'Ajouter automatiquement les room servers';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts';
|
||||
'Autoriser le compagnon à ajouter automatiquement les room servers découverts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
@@ -3596,6 +3695,76 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d\'antenne';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Traduction';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Activer la traduction';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Traduire les messages entrants et permettre la traduction avant l\'envoi.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Traduire avant d\'envoyer';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Contrôle l\'état par défaut de l\'icône de traduction du composant.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Langue cible';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage =>
|
||||
'Utiliser la langue de l\'application';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modèle téléchargé';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => 'Modèle Hugging Face préconfiguré';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL du modèle manuel';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Télécharger le modèle';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Téléchargement...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Au travail...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Arrêtez';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Fusion des fragments téléchargés dans le fichier final...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modèles téléchargés';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Supprimer le modèle';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modèle de traduction téléchargé.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped =>
|
||||
'Le téléchargement a été interrompu.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Échec du téléchargement : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Entrez d\'abord l\'URL du modèle.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Afficher le code PIN';
|
||||
|
||||
@@ -3603,10 +3772,72 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Code PIN d’appairage Bluetooth';
|
||||
String get scanner_linuxPairingPinTitle =>
|
||||
'Code PIN pour la connexion Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Entrez le code PIN pour $deviceName (laissez vide si aucun).';
|
||||
return 'Entrez le code PIN pour $deviceName (laissez vide si nécessaire).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Traduction du message';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Traduire avant d\'envoyer';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Les messages seront traduits avant d\'être envoyés.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Envoyez des messages dans la langue originale, telle que vous l\'avez tapée.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Traduire en $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Options de traduction';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Langue du système';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Bezárás';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Szerkesztés';
|
||||
|
||||
@@ -437,9 +440,7 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
'A hirdetés tartalmazza a helyszínt.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Többszöri visszaigazolások: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Többszörös visszaigazolások';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'A telemetriamód frissítve';
|
||||
@@ -447,6 +448,13 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Tevékenységek';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Hirdetés küldése';
|
||||
|
||||
@@ -488,11 +496,11 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get settings_debug => 'Hibakeresés';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE hibaelhárítási napló';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'BLE parancsok, válaszok és alapvető adatok';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'App-debug log';
|
||||
@@ -1099,6 +1107,11 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'A nyilvánosság számára';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Személyes';
|
||||
|
||||
@@ -1246,6 +1259,14 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Még nincs üzenet.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Üzenet küldése';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Küldj üzenetet $contactName-nek';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Küldj egy üzenetet, hogy elindulj!';
|
||||
|
||||
@@ -1265,11 +1286,6 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Helyszín';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Küldj üzenetet $contactName-nek';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Írjon üzenetet...';
|
||||
|
||||
@@ -1464,6 +1480,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@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';
|
||||
|
||||
@@ -1564,6 +1583,12 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
return 'Olvasatlan: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Nyisd meg a linket?';
|
||||
|
||||
@@ -1646,6 +1671,24 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Zászló';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Osztja ezt a tartalmat itt';
|
||||
|
||||
@@ -1732,6 +1775,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Gemeinsames PIN-kód';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Megosztva';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Csatlakozás a szobához';
|
||||
|
||||
@@ -2027,9 +2073,18 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Szoba-szerver kezelés';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Adatok a repeaterről';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Szoba szerver információk';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Menedzsmentes eszközök';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Vendégek számára elérhető eszközök';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Állapot';
|
||||
|
||||
@@ -2063,6 +2118,14 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Óra szinkronizálás bejelentkezés után';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Adatkapcsolódás állapot';
|
||||
|
||||
@@ -2437,6 +2500,12 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'óra';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Óra szinkronizálás';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Fedezd fel a szomszédokat';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot';
|
||||
|
||||
@@ -3225,6 +3294,37 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terület';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Foglalhatatlan területek';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'A blokkolt területet megjelölve, hogy a térképen kiemeljük.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Kiválasztott akadály';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Elakadt a $obstruction miatt, $heightUnit magasságban, $distanceFromA méterrel A-tól és $distanceFromB méterrel B-től ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Hatósság';
|
||||
|
||||
@@ -3588,10 +3688,80 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
'Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'PIN megjelenítése';
|
||||
String get translation_title => 'Fordítás';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'PIN elrejtése';
|
||||
String get translation_enableTitle => 'Engedje meg a fordítást';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Fordítsa az érkező üzeneteket, és lehetővé tegye a küldés előtti fordítást.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Fordítsa el, mielőtt elküldi';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Ellenőrzi a zeneszerző fordítási ikon alapértékét.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Célnyelv';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage =>
|
||||
'Használja az alkalmazás nyelvének beállítását.';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Letöltött modell';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Előre definiált Hugging Face-modell';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Manuális modell URL';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Letöltés';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Letöltés...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Munkában vagyok...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Halt';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'A letöltött részek összeállítása a végleges fájlba...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Letöltött modelok';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Törölje a modellt';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Fordítási modell letöltve.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'A letöltés leállt.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Letöltés sikertelen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Addon először egy modell URL-t.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Megjelenítse a PIN-kódot';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Rejtse el a PIN-kódot';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Bluetooth párosítási PIN';
|
||||
@@ -3600,4 +3770,66 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Üzenet fordítása';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Fordítsa el, mielőtt elküldi';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'A üzenetek fordítását a küldés előtt elvégezzük.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Küldj üzeneteket az eredeti, nyomtatott nyelven.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Fordítás $language-ra';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Fordítási lehetőségek';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Rendszer nyelvé';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Chiudi';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Modifica';
|
||||
|
||||
@@ -437,9 +440,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'Includi la posizione nell\'annuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
String get settings_multiAck => 'ACK multipli';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
|
||||
@@ -447,6 +448,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Azioni';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Invia Annuncio';
|
||||
|
||||
@@ -488,11 +496,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_debug => 'Risoluzione dei problemi';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Log di Debug BLE';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Comandi, risposte e dati grezzi BLE';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Log di Debug dell\'App';
|
||||
@@ -1095,6 +1103,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Pubblico';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privato';
|
||||
|
||||
@@ -1239,6 +1252,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Nessun messaggio ancora';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Invia messaggio';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Invia un messaggio a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Invia un messaggio per iniziare';
|
||||
|
||||
@@ -1258,11 +1279,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Posizione';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Invia un messaggio a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Digita un messaggio...';
|
||||
|
||||
@@ -1453,6 +1469,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'successi';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Rimuovi percorso';
|
||||
|
||||
@@ -1555,6 +1574,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Non letti: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Aprire il link?';
|
||||
|
||||
@@ -1636,6 +1661,24 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Bandiere';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Condividi marcatore qui';
|
||||
|
||||
@@ -1720,6 +1763,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Condividi PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Condiviso';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Unisciti alla stanza';
|
||||
|
||||
@@ -2013,9 +2059,18 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Gestione del Server di Camera';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informazioni sul ripetitore';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informazioni sul server';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Strumenti di Gestione';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Strumenti per gli ospiti';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Stato';
|
||||
|
||||
@@ -2050,6 +2105,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Configura i parametri del ripetitore';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Sincronizzazione dell\'orologio dopo il login';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Stato del Ripetitore';
|
||||
|
||||
@@ -2426,6 +2489,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Orologio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Scopri i Vicini';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario';
|
||||
|
||||
@@ -3214,6 +3283,37 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Posti occupati';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Tocca un punto bloccato sulla mappa per evidenziarlo.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Ostacolo selezionato';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequenza';
|
||||
|
||||
@@ -3574,17 +3674,149 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Traduzione';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Abilitare la traduzione';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell\'invio.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Tradurre prima di inviare';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controlla lo stato predefinito dell\'icona di traduzione del compositore.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Lingua di destinazione';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Utilizza la lingua dell\'app';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modello scaricato';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Modello predefinito di Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL del modello manuale';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Scarica il modello';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Inizio download...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Lavoro...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Smetta';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Unione dei frammenti scaricati in un unico file...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modelli scaricati';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Elimina modello';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modello di traduzione scaricato.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Il download è stato interrotto.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Inserite innanzitutto l\'URL del modello.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Mostra PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Nascondi PIN';
|
||||
String get scanner_linuxPairingHidePin => 'Nascondi il PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'PIN di associazione Bluetooth';
|
||||
String get scanner_linuxPairingPinTitle =>
|
||||
'PIN per l\'accoppiamento Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Inserisci il PIN per $deviceName (lascia vuoto se non ce n\'è).';
|
||||
return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Traduzione del messaggio';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Tradurre prima di inviare';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'I messaggi verranno tradotti prima di essere inviati.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Invia messaggi utilizzando la lingua originale, scritta.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Tradurre in $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opzioni di traduzione';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Lingua del sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => '閉じる';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => '編集';
|
||||
|
||||
@@ -414,9 +417,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '複数のACK:$value';
|
||||
}
|
||||
String get settings_multiAck => '複数のACK(応答)';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'テレメトリモードが更新されました';
|
||||
@@ -424,6 +425,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => '行動';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => '広告を送信する';
|
||||
|
||||
@@ -462,10 +470,11 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get settings_debug => 'デバッグ';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE デバッグログ';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle => 'BLEコマンド、応答、および生のデータ';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'アプリケーションのデバッグログ';
|
||||
@@ -1041,6 +1050,11 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => '一般の人々';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => '個人の';
|
||||
|
||||
@@ -1179,6 +1193,14 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'まだメッセージは届いていません';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'メッセージを送信する';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '$contactName へのメッセージを送信する';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => '開始するためにメッセージを送信してください';
|
||||
|
||||
@@ -1198,11 +1220,6 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => '場所';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '$contactName へのメッセージを送信する';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'メッセージを入力してください…';
|
||||
|
||||
@@ -1391,6 +1408,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => '成功事例';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'パスを削除する';
|
||||
|
||||
@@ -1486,6 +1506,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '未読: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'リンクを開く?';
|
||||
|
||||
@@ -1564,6 +1590,24 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => '旗';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'この場所でシェア';
|
||||
|
||||
@@ -1647,6 +1691,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => '共有パスワード';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '共有済み';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '部屋に参加する';
|
||||
|
||||
@@ -1929,9 +1976,18 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'ルームサーバーの管理';
|
||||
|
||||
@override
|
||||
String get repeater_guest => '繰り返し送信に関する情報';
|
||||
|
||||
@override
|
||||
String get room_guest => 'ルームサーバーに関する情報';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => '管理ツール';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'ゲスト向けツール';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'ステータス';
|
||||
|
||||
@@ -1962,6 +2018,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'ログインが成功した場合、自動的に「時刻同期」を送信する。';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => '再送ステータス';
|
||||
|
||||
@@ -2322,6 +2385,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => '時計';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'クロック同期';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => '近隣を発見する';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => '広告用資料を送る';
|
||||
|
||||
@@ -3067,6 +3136,36 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => '地形';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => '利用できない場所';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint => '地図上で、特定された場所を強調するために、該当する場所をタップしてください。';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => '選択された障害';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return '$obstruction によって $heightUnit の高さで、A地点から $distanceFromA、B地点から $distanceFromB ($distanceUnit) の距離で塞がれています。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => '周波数';
|
||||
|
||||
@@ -3402,6 +3501,71 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get radioStats_settingsSubtitle => 'ノイズレベル、RSSI、SNR、および通信時間';
|
||||
|
||||
@override
|
||||
String get translation_title => '翻訳';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => '翻訳機能を有効にする';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle => '受信メッセージを翻訳し、送信前に翻訳を適用できるようにする。';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => '送信する前に翻訳する';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle => '作曲家翻訳アイコンのデフォルト状態を制御する。';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => '翻訳対象言語';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'アプリの言語設定';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'ダウンロードしたモデル';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => 'あらかじめ設定されたHugging Faceモデル';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'マニュアルモデルのURL';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'モデルのダウンロード';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'ダウンロード中...';
|
||||
|
||||
@override
|
||||
String get translation_working => '業務中…';
|
||||
|
||||
@override
|
||||
String get translation_stop => '停止';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks => 'ダウンロードしたファイルを最終ファイルに結合中...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'ダウンロードされたモデル';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'モデルを削除';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => '翻訳モデルのダウンロードが完了しました。';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'ダウンロードが中断されました。';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'ダウンロードに失敗しました:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'まず、モデルのURLを入力してください。';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'PINを表示';
|
||||
|
||||
@@ -3415,4 +3579,63 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return '$deviceNameのPINを入力してください(なしの場合は空欄のまま)。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'メッセージの翻訳';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => '送信する前に翻訳する';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint => 'メッセージは送信前に翻訳されます。';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint => '元のタイプされた言語でメッセージを送信してください。';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return '$language への翻訳';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => '翻訳の選択肢';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'システム言語';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => '닫기';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => '수정';
|
||||
|
||||
@@ -414,9 +417,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '다중 ACK: $value';
|
||||
}
|
||||
String get settings_multiAck => '다중 ACK';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => '텔레메트리 모드 업데이트 완료';
|
||||
@@ -424,6 +425,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => '행동';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => '광고 전송';
|
||||
|
||||
@@ -462,10 +470,11 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get settings_debug => '디버깅';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE 디버그 로그';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle => 'BLE 명령어, 응답 및 원시 데이터';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => '앱 디버깅 로그';
|
||||
@@ -1036,6 +1045,11 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => '대중의';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => '사립';
|
||||
|
||||
@@ -1174,6 +1188,14 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => '아직 메시지가 없습니다.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => '메시지를 보내기';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '$contactName에게 메시지를 보내';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => '시작하려면 메시지를 보내세요.';
|
||||
|
||||
@@ -1193,11 +1215,6 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => '위치';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '$contactName에게 메시지를 보내';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => '메시지를 입력하세요...';
|
||||
|
||||
@@ -1387,6 +1404,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => '성공 사례';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => '경로 제거';
|
||||
|
||||
@@ -1482,6 +1502,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
return '읽지 않음: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => '링크를 열기?';
|
||||
|
||||
@@ -1560,6 +1586,24 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => '깃발';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => '여기에서 마커 공유';
|
||||
|
||||
@@ -1643,6 +1687,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => '공유 비밀번호';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '공유됨';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '방에 참여';
|
||||
|
||||
@@ -1926,9 +1973,18 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => '방 서버 관리';
|
||||
|
||||
@override
|
||||
String get repeater_guest => '반복 장비 정보';
|
||||
|
||||
@override
|
||||
String get room_guest => '서버 정보';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => '관리 도구';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => '손님용 도구';
|
||||
|
||||
@override
|
||||
String get repeater_status => '상태';
|
||||
|
||||
@@ -1959,6 +2015,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => '리피터 파라미터 설정';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => '로그인 후 시계 동기화';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => '반복 장치 상태';
|
||||
|
||||
@@ -2319,6 +2382,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => '시계';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => '시계 동기화';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => '이웃 발견하기';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => '광고 패킷을 발송';
|
||||
|
||||
@@ -3067,6 +3136,36 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => '지형';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => '차단된 공간';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint => '지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => '선택된 장애물';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => '빈도';
|
||||
|
||||
@@ -3402,7 +3501,72 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle => '잡음 수준, RSSI, 신호 대 잡음비, 통신 시간';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'PIN 표시';
|
||||
String get translation_title => '번역';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => '번역 기능 활성화';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle => '입력 메시지를 번역하고, 미리 번역 기능을 제공합니다.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => '보내기 전에 번역';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle => '컴포저 번역 아이콘의 기본 상태를 제어합니다.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => '목표 언어';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => '앱 언어 사용';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => '다운로드한 모델';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => '사전에 설정된 Hugging Face 모델';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => '수동 모델 URL';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => '모델 다운로드';
|
||||
|
||||
@override
|
||||
String get translation_downloading => '다운로드 중...';
|
||||
|
||||
@override
|
||||
String get translation_working => '업무 중...';
|
||||
|
||||
@override
|
||||
String get translation_stop => '멈춰';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks => '다운로드한 파일 조각들을 최종 파일로 병합 중...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => '다운로드한 모델';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => '모델 삭제';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => '번역 모델이 다운로드되었습니다.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => '다운로드 중단됨.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return '다운로드 실패: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => '먼저 모델 URL을 입력하세요.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'PIN 보기';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'PIN 숨기기';
|
||||
@@ -3412,6 +3576,65 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return '$deviceName에 대한 PIN을 입력하세요 (없으면 비워두세요).';
|
||||
return '$deviceName의 PIN을 입력하세요 (해당하는 경우에만 입력).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => '메시지 번역';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => '보내기 전에 번역';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint => '메시지는 전송하기 전에 번역될 것입니다.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint => '원래 작성된 언어로 메시지를 보내세요.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return '$language 번역';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => '번역 옵션';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => '시스템 언어';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Sluiten';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Bewerken';
|
||||
|
||||
@@ -313,7 +316,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_nodeSettings => 'Node Instellingen';
|
||||
|
||||
@override
|
||||
String get settings_nodeName => 'Node Naam';
|
||||
String get settings_nodeName => 'Nodenaam';
|
||||
|
||||
@override
|
||||
String get settings_nodeNameNotSet => 'Niet ingesteld';
|
||||
@@ -432,9 +435,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Locatie opnemen in advertentie';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Meerdere bevestigingen';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
|
||||
@@ -442,6 +443,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Acties';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Verzend Advertentie';
|
||||
|
||||
@@ -452,7 +460,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_advertisementSent => 'Advertentie verzonden';
|
||||
|
||||
@override
|
||||
String get settings_syncTime => 'Synchronisatie Tijd';
|
||||
String get settings_syncTime => 'Tijd Synchroniseren';
|
||||
|
||||
@override
|
||||
String get settings_syncTimeSubtitle =>
|
||||
@@ -472,7 +480,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_rebootDevice => 'Apparaat opnieuw opstarten';
|
||||
|
||||
@override
|
||||
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore apparaat';
|
||||
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore-apparaat';
|
||||
|
||||
@override
|
||||
String get settings_rebootDeviceConfirm =>
|
||||
@@ -482,11 +490,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_debug => 'Debug';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE Debug Log';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'BLE commando\'s, antwoorden en ruwe data';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'App Debug Log';
|
||||
@@ -556,7 +564,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_codingRate => 'Codeertarief';
|
||||
|
||||
@override
|
||||
String get settings_txPower => 'TX Vermogen (dBm)';
|
||||
String get settings_txPower => 'TX-Vermogen (dBm)';
|
||||
|
||||
@override
|
||||
String get settings_txPowerHelper => '0 - 22';
|
||||
@@ -565,11 +573,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
|
||||
String get settings_clientRepeat => 'Off-Grid Herhalen';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
|
||||
'Laat dit apparaat de berichten van andere apparaten doorsturen.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
@@ -846,19 +854,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get appSettings_allTime => 'Altijd';
|
||||
|
||||
@override
|
||||
String get appSettings_lastHour => 'Laat uur';
|
||||
String get appSettings_lastHour => 'Afgelopen uur';
|
||||
|
||||
@override
|
||||
String get appSettings_last6Hours => 'laatste 6 uur';
|
||||
String get appSettings_last6Hours => 'Afgelopen 6 uur';
|
||||
|
||||
@override
|
||||
String get appSettings_last24Hours => 'De laatste 24 uur';
|
||||
String get appSettings_last24Hours => 'Afgelopen 24 uur';
|
||||
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Laatste week';
|
||||
String get appSettings_lastWeek => 'Afgelopen week';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
|
||||
String get appSettings_offlineMapCache => 'Offline Kaartcache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Eenheden';
|
||||
@@ -1084,6 +1092,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Openbaar';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privé';
|
||||
|
||||
@@ -1185,32 +1198,32 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get channels_sortUnread => 'Ongelezen';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
|
||||
String get channels_createPrivateChannel => 'PrivéKanaal Aanmaken';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Beveiligd met een geheime sleutel.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
|
||||
String get channels_joinPrivateChannel => 'PrivéKanaal Toetreden';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Handmatig een geheime sleutel invoeren.';
|
||||
'Voer handmatig een geheime sleutel in.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
|
||||
String get channels_joinPublicChannel => 'Publiek Kanaal Toetreden';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Iedereen kan dit kanaal aanmelden.';
|
||||
'Iedereen kan toetreden tot dit kanaal.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
|
||||
String get channels_joinHashtagChannel => 'Hashtag-kanaal Aanmaken';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Iedereen kan lid worden van hashtag-kanalen.';
|
||||
'Iedereen kan toetreden tot hashtag-kanalen.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scan een QR-code';
|
||||
@@ -1227,6 +1240,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Nog geen berichten.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Verzend bericht';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Verstuur een bericht naar $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Een bericht sturen om te beginnen';
|
||||
|
||||
@@ -1246,11 +1267,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Locatie';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Verstuur een bericht naar $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Type een bericht...';
|
||||
|
||||
@@ -1441,6 +1457,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Succesvol';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Pad verwijderen';
|
||||
|
||||
@@ -1542,6 +1561,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Nieuw: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Link openen?';
|
||||
|
||||
@@ -1623,6 +1648,24 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Vlaggen';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Deel marker hier';
|
||||
|
||||
@@ -1709,7 +1752,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get map_sharedPin => 'Gedeelde pin';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Sluit Kamer';
|
||||
String get map_sharedAt => 'Gedeeld';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Kamer Toetreden';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'Beheer Repeater';
|
||||
@@ -2001,7 +2047,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get room_management => 'Beheer Server Kamer';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Beheerinstrumenten';
|
||||
String get repeater_guest => 'Informatie over herhalingsapparatuur';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informatie over de server';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Beheerfuncties';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Gastenfuncties';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
@@ -2027,7 +2082,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_neighbors => 'Buren';
|
||||
|
||||
@override
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul-hopsburen.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Instellingen';
|
||||
@@ -2035,6 +2090,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configureer repeaterparameters';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Na het inloggen, klok synchroniseren';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status repeater';
|
||||
|
||||
@@ -2093,10 +2156,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_noiseFloor => 'Ruisvloer';
|
||||
|
||||
@override
|
||||
String get repeater_txAirtime => 'TX Airtime';
|
||||
String get repeater_txAirtime => 'TX-zendtijd';
|
||||
|
||||
@override
|
||||
String get repeater_rxAirtime => 'RX Airtime';
|
||||
String get repeater_rxAirtime => 'RX-zendtijd';
|
||||
|
||||
@override
|
||||
String get repeater_packetStatistics => 'Pakketstatistieken';
|
||||
@@ -2141,7 +2204,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_settingsTitle => 'Repeater Instellingen';
|
||||
String get repeater_settingsTitle => 'Repeaterinstellingen';
|
||||
|
||||
@override
|
||||
String get repeater_basicSettings => 'Basisinstellingen';
|
||||
@@ -2150,19 +2213,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_repeaterName => 'Repeaternaam';
|
||||
|
||||
@override
|
||||
String get repeater_repeaterNameHelper => 'Weergave naam voor deze repeater';
|
||||
String get repeater_repeaterNameHelper => 'Weergavenaam voor deze repeater';
|
||||
|
||||
@override
|
||||
String get repeater_adminPassword => 'Admin wachtwoord';
|
||||
|
||||
@override
|
||||
String get repeater_adminPasswordHelper => 'Volledige toegangspaswoord';
|
||||
String get repeater_adminPasswordHelper => 'Wachtwoord administratortoegang';
|
||||
|
||||
@override
|
||||
String get repeater_guestPassword => 'Wachtwoord Gast';
|
||||
String get repeater_guestPassword => 'Gast wachtwoord';
|
||||
|
||||
@override
|
||||
String get repeater_guestPasswordHelper => 'Leesbeheer wachtwoord';
|
||||
String get repeater_guestPasswordHelper => 'Wachtwoord gasttoegen';
|
||||
|
||||
@override
|
||||
String get repeater_radioSettings => 'Radio Instellingen';
|
||||
@@ -2189,7 +2252,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_codingRate => 'Codeertarief';
|
||||
|
||||
@override
|
||||
String get repeater_locationSettings => 'Locatie Instellingen';
|
||||
String get repeater_locationSettings => 'Locatie-instellingen';
|
||||
|
||||
@override
|
||||
String get repeater_latitude => 'Breedtegraad';
|
||||
@@ -2221,14 +2284,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Toegestane leesbeheer toegang voor gasten.';
|
||||
|
||||
@override
|
||||
String get repeater_privacyMode => 'Privacy Modus';
|
||||
String get repeater_privacyMode => 'Privacymodus';
|
||||
|
||||
@override
|
||||
String get repeater_privacyModeSubtitle =>
|
||||
'Naam/locatie verbergen in advertenties';
|
||||
|
||||
@override
|
||||
String get repeater_advertisementSettings => 'Advertentie Instellingen';
|
||||
String get repeater_advertisementSettings => 'Advertentie-instellingen';
|
||||
|
||||
@override
|
||||
String get repeater_localAdvertInterval => 'Lokale Advertentie Interval';
|
||||
@@ -2333,7 +2396,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_refreshGuestAccess => 'Toegang Gast Vernieuwen';
|
||||
|
||||
@override
|
||||
String get repeater_refreshPrivacyMode => 'Privacy Mode vernieuwen';
|
||||
String get repeater_refreshPrivacyMode => 'Privacymode vernieuwen';
|
||||
|
||||
@override
|
||||
String get repeater_refreshAdvertisementSettings =>
|
||||
@@ -2359,10 +2422,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_commandHelp => 'Help';
|
||||
|
||||
@override
|
||||
String get repeater_clearHistory => 'Verwijder Geschiedenis';
|
||||
String get repeater_clearHistory => 'Geschiedenis Verwijderen';
|
||||
|
||||
@override
|
||||
String get repeater_noCommandsSent => 'Geen commando\'s verzonden nog.';
|
||||
String get repeater_noCommandsSent => 'Nog geen commando\'s verzonden.';
|
||||
|
||||
@override
|
||||
String get repeater_typeCommandOrUseQuick =>
|
||||
@@ -2389,28 +2452,34 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetName => 'Haal Naam op';
|
||||
String get repeater_cliQuickGetName => 'Naam opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetRadio => 'Radio ontvangen';
|
||||
String get repeater_cliQuickGetRadio => 'Radio-instellingen opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetTx => 'Krijg TX';
|
||||
String get repeater_cliQuickGetTx => 'TX opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickNeighbors => 'Buren';
|
||||
String get repeater_cliQuickNeighbors => 'Buren opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickVersion => 'Versie';
|
||||
String get repeater_cliQuickVersion => 'Versie opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickAdvertise => 'Advertenties';
|
||||
String get repeater_cliQuickAdvertise => 'Advertenties opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Tijd';
|
||||
String get repeater_cliQuickClock => 'Tijd opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket';
|
||||
String get repeater_cliQuickClockSync => 'Kloksynchronisatie';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Ontdek Buren';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Advertentie uitzenden';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpReboot =>
|
||||
@@ -2682,7 +2751,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get telemetry_voltageLabel => 'Spanning';
|
||||
|
||||
@override
|
||||
String get telemetry_mcuTemperatureLabel => 'MCU Temperatuur';
|
||||
String get telemetry_mcuTemperatureLabel => 'MCU-temperatuur';
|
||||
|
||||
@override
|
||||
String get telemetry_temperatureLabel => 'Temperatuur';
|
||||
@@ -2723,7 +2792,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
|
||||
String get neighbors_repeatersNeighbors => 'Repeatbburen';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
||||
@@ -3022,7 +3091,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get listFilter_latestMessages => 'Recente berichten';
|
||||
|
||||
@override
|
||||
String get listFilter_heardRecently => 'Hoor je onlangs';
|
||||
String get listFilter_heardRecently => 'Recent gezien';
|
||||
|
||||
@override
|
||||
String get listFilter_az => 'A-Z';
|
||||
@@ -3196,6 +3265,37 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrein';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Geplande plaatsen';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Tik op een geblokkeerd gebied om het op de kaart te markeren.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Geselecteerde obstakel';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequentie';
|
||||
|
||||
@@ -3268,7 +3368,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Contact uit klembord toevoegen';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
|
||||
String get contacts_ShareContact => 'Contact naar Klembord kopiëren';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
|
||||
@@ -3552,6 +3652,76 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Ruimtelijke ruis, RSSI, SNR en beschikbare tijd';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Vertaling';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Activeer vertaling';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Vertaal voor verzending';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Doeltaal';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Gebruik de taal van de app';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Gedownloade model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Voorgeprogrammeerd Hugging Face-model';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL van de handleiding';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Download het model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Downloaden...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Werken...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stoppen';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Het samenvoegen van de gedownloade stukken tot één eindbestand...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Gedownloade modellen';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Model verwijderen';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Vertalingmodel gedownload.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Download is afgebroken.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download mislukt: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Voer eerst een URL van een model in.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Toon PIN';
|
||||
|
||||
@@ -3565,4 +3735,65 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Voer PIN in voor $deviceName (laat leeg als er geen is).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Berichtvertaling';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Vertaal voor verzending';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'De berichten worden vertaald voordat ze verzonden worden.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Stuur berichten in de oorspronkelijke, getypte taal.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Vertalen naar $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opties voor vertaling';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Taal van het systeem';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Zamknij';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Edytuj';
|
||||
|
||||
@@ -439,9 +442,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
'Uwzględnij lokalizację w ogłoszeniu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Wiele potwierdzeń: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
@@ -450,6 +451,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Działania';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Wyślij rozgłoszenie';
|
||||
|
||||
@@ -490,11 +498,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_debug => 'Debug';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Dziennik debugowania BLE';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Polecenia BLE, odpowiedzi i surowe dane';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Dziennik debugowania aplikacji';
|
||||
@@ -611,7 +619,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get appSettings_language => 'Język';
|
||||
|
||||
@override
|
||||
String get appSettings_languageSystem => 'Domyślny systemu';
|
||||
String get appSettings_languageSystem => 'Domyślny systemowy';
|
||||
|
||||
@override
|
||||
String get appSettings_languageEn => 'English';
|
||||
@@ -1104,6 +1112,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Publiczny';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Prywatny';
|
||||
|
||||
@@ -1247,6 +1260,14 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Brak jeszcze wiadomości';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Wyślij wiadomość';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Wyślij wiadomość do $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.';
|
||||
|
||||
@@ -1267,11 +1288,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Lokalizacja';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Wyślij wiadomość do $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Wpisz wiadomość...';
|
||||
|
||||
@@ -1465,6 +1481,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Sukcesy';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Usuń ścieżkę';
|
||||
|
||||
@@ -1566,6 +1585,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Nieprzeczytane: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Otworzyć link?';
|
||||
|
||||
@@ -1647,6 +1672,24 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flagi';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
|
||||
|
||||
@@ -1699,7 +1742,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get map_otherNodes => 'Inne węzły';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Nakładające się klucze powtarzalne';
|
||||
String get map_showOverlaps => 'Nakładające się klucze przekaźników';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Prefiks klucza';
|
||||
@@ -1732,6 +1775,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Udostępniona pinezka';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Udostępnione';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Dołącz do pokoju';
|
||||
|
||||
@@ -1745,7 +1791,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get map_runTrace => 'Uruchom ślad ścieżki';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Wróć z powrotem tą samą ścieżką';
|
||||
String get map_runTraceWithReturnPath => 'Wróć tą samą ścieżką';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Usuń ostatni';
|
||||
@@ -2028,9 +2074,18 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Zarządzanie Serwerem Pokoju';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informacje dotyczące urządzenia powtarzającego';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informacje o serwerze';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Narzędzia Zarządzania';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Narzędzia dla gości';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -2055,8 +2110,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get repeater_neighbors => 'Sąsiedzi';
|
||||
|
||||
@override
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Wyświetl sąsiedztwo zerowych hopów.';
|
||||
String get repeater_neighborsSubtitle => 'Wyświetl sąsiadów zero-hop.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Ustawienia';
|
||||
@@ -2064,6 +2118,14 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Synchronizacja zegara po zalogowaniu';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status przekaźnika';
|
||||
|
||||
@@ -2436,6 +2498,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Godzina';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Synchronizacja zegara';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy';
|
||||
|
||||
@@ -3222,6 +3290,37 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Teren';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Zablokowane miejsca';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Wybór przeszkody';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Częstotliwość';
|
||||
|
||||
@@ -3368,11 +3467,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Eksportuj przekaźniki / serwer pokojowy do GPX';
|
||||
'Eksportuj przekaźniki / roomservery do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.';
|
||||
'Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
|
||||
@@ -3404,7 +3503,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Lokalizacje przekaźników i serwerów pokojowych';
|
||||
'Lokalizacje przekaźników i roomserverów';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
|
||||
@@ -3421,7 +3520,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
'Eksport danych mapy GPX meshcore-open';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
|
||||
String get snrIndicator_nearByRepeaters => 'Pobliskie przekaźniki';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ostatnio widziany';
|
||||
@@ -3454,11 +3553,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatycznie dodaj serwery pokojowe';
|
||||
'Automatycznie dodaj roomservery';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
|
||||
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
@@ -3586,6 +3685,74 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Szum tła, RSSI, SNR oraz czas dostępny';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Tłumaczenie';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Włącz tłumaczenie';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Przekład przed wysłaniem';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Kontroluje domyślny stan ikony tłumaczenia w edytorze.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Język docelowy';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Użyj języka aplikacji';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Pobudowany model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => 'Wspólny model Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Adres URL do wersji manualnej';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Pobierz model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Pobieranie...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Praca...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Zatrzymaj się';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Scalanie pobranych fragmentów w jeden plik końcowy...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Pobrane modele';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Usuń model';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Model tłumaczenia został pobrany.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Pobieranie zakończone.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Nie udało się pobrać: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Najpierw wprowadź adres URL modelu.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Pokaż PIN';
|
||||
|
||||
@@ -3599,4 +3766,65 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Wprowadź kod PIN dla $deviceName (pozostaw puste, jeśli brak).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Tłumaczenie wiadomości';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Przekład przed wysłaniem';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Komunikaty zostaną przetłumaczone przed wysłaniem.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Wysyłaj wiadomości w oryginalnym, wpisanym formacie.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Tłumacz na $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opcje tłumaczenia';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Język systemu';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Fechar';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Editar';
|
||||
|
||||
@@ -436,9 +439,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
'Incluir localização no anúncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
|
||||
@@ -446,6 +447,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Ações';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Enviar Publicidade';
|
||||
|
||||
@@ -488,11 +496,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_debug => 'Depurar';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Log de Depuração BLE';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Comandos, respostas e dados brutos do BLE';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Log de Depuração do Aplicativo';
|
||||
@@ -1095,6 +1103,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Público';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privado';
|
||||
|
||||
@@ -1238,6 +1251,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Ainda não existem mensagens.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Enviar mensagem';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar uma mensagem para $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Enviar uma mensagem para começar';
|
||||
|
||||
@@ -1257,11 +1278,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Localização';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar uma mensagem para $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Digite uma mensagem...';
|
||||
|
||||
@@ -1452,6 +1468,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Sucessos';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Remover caminho';
|
||||
|
||||
@@ -1553,6 +1572,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Não lido: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Abrir link?';
|
||||
|
||||
@@ -1635,6 +1660,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Bandeiras';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
|
||||
|
||||
@@ -1720,6 +1763,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Pin compartilhado';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Compartilhado';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Junte-se à Sala';
|
||||
|
||||
@@ -2012,9 +2058,18 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Gerenciamento de Servidor de Sala';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informações sobre repetidores';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informações do Servidor';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Ferramentas para hóspedes';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -2047,6 +2102,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Sincronização do relógio após o login';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status do Repetidor';
|
||||
|
||||
@@ -2423,6 +2486,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Relógio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Sincronização do Relógio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios';
|
||||
|
||||
@@ -3210,6 +3279,37 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Locais ocupados';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Toque em um ponto bloqueado para destacá-lo no mapa.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Obstrução selecionada';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequência';
|
||||
|
||||
@@ -3567,6 +3667,75 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Nível de ruído, RSSI, SNR e tempo de transmissão';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Tradução';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Ativar a tradução';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Traduzir mensagens recebidas e permitir a tradução antes do envio.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Traduza antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controla o estado padrão do ícone de tradução do compositor.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Língua-alvo';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Utilize o idioma da aplicação';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modelo baixado';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Modelo pré-definido da Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL do modelo manual';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Baixar modelo';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Baixando...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Trabalhando...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Pare';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Combinando os fragmentos baixados em um único arquivo...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modelos baixados';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Excluir modelo';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modelo de tradução baixado.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Download interrompido.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Falha na descarga: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Insira primeiro a URL do modelo.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
|
||||
|
||||
@@ -3580,4 +3749,65 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Insira o PIN para $deviceName (deixe em branco se não houver).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Tradução da mensagem';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Traduzir antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'As mensagens serão traduzidas antes de serem enviadas.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Envie mensagens no idioma original, conforme digitado.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Traduzir para $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opções de tradução';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Idioma do sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Закрыть';
|
||||
|
||||
@override
|
||||
String get common_done => 'Готово';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Изменить';
|
||||
|
||||
@@ -436,9 +439,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
'Включить местоположение в объявление';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мульти-ACK: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Несколько подтверждений';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
|
||||
@@ -446,6 +447,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Отправить анонсирование';
|
||||
|
||||
@@ -487,11 +495,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get settings_debug => 'Отладка';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Журнал отладки BLE';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Команды BLE, ответы и сырые данные';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Журнал отладки приложения';
|
||||
@@ -1095,6 +1103,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Публичный';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'через $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Приватный';
|
||||
|
||||
@@ -1238,6 +1251,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Сообщений пока нет';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Отправить сообщение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Отправить сообщение $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать';
|
||||
|
||||
@@ -1257,11 +1278,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Местоположение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Отправить сообщение $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Напишите сообщение...';
|
||||
|
||||
@@ -1455,6 +1471,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'успешно';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Оценка';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Удалить маршрут';
|
||||
|
||||
@@ -1557,6 +1576,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Непрочитанных: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Открыть ссылку?';
|
||||
|
||||
@@ -1638,6 +1663,24 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Флаги';
|
||||
|
||||
@override
|
||||
String get map_type => 'Тип';
|
||||
|
||||
@override
|
||||
String get map_path => 'Путь';
|
||||
|
||||
@override
|
||||
String get map_location => 'Местоположение';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Прибл. местоположение';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Публичный ключ';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'напр. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
||||
|
||||
@@ -1723,6 +1766,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Общая метка';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Поделено';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Присоединиться к комнате';
|
||||
|
||||
@@ -2016,9 +2062,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Управление сервером комнат';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Информация о ретрансляторе';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Информация о сервере';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Инструменты управления';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Инструменты для гостей';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Статус';
|
||||
|
||||
@@ -2051,6 +2106,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Настройка параметров репитера';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Синхронизация часов после входа в систему';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Статус репитера';
|
||||
|
||||
@@ -2427,6 +2490,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Время';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Синхронизация часов';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Обнаружить Соседей';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования';
|
||||
|
||||
@@ -3215,6 +3284,38 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Рельеф';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Зарезервированные места';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Щелкните по заблокированной области, чтобы выделить ее на карте.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle =>
|
||||
'Выбранный объект, препятствующий движению';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Частота';
|
||||
|
||||
@@ -3581,6 +3682,75 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Уровень шума, RSSI, SNR и время передачи';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Перевод';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Включить перевод';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Переводить входящие сообщения и позволять предварительный перевод перед отправкой.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Переводить перед отправкой';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Управляет исходным состоянием значка перевода, предоставляемого редактором.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Целевой язык';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Используйте язык приложения';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Загруженная модель';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Предопределенная модель от Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Ссылка на руководство';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Скачать модель';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Загрузка...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Работа...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Прекратите';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Объединение скачанных фрагментов в один финальный файл...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Загруженные модели';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Удалить модель';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Модель перевода загружена.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Процесс загрузки был прерван.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Не удалось скачать: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Сначала введите URL модели.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Показать PIN';
|
||||
|
||||
@@ -3594,4 +3764,65 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Введите PIN‑код для $deviceName (оставьте пустым, если нет).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Перевод сообщения';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Перевести перед отправкой';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Сообщения будут переведены перед отправкой.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Отправляйте сообщения на языке, в котором они были изначально набраны.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Перевести на $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Варианты перевода';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Язык системы';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore работает';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Поддерживает BLE-соединение';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Удалено $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Не удалось удалить: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Не удалось обновить канал: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Чат';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Ретранслятор';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Комната';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Датчик';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Неизвестно';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Zavrieť';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Upraviť';
|
||||
|
||||
@@ -430,9 +433,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Viaceré ACK: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Viaceré ACK';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
@@ -441,6 +442,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Možné akcie';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Odoslať reklamu';
|
||||
|
||||
@@ -482,11 +490,11 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get settings_debug => 'Ladenie';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'Log BLE Debug';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'Príkazy BLE, odpovede a surové dáta';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Záznam ladenia aplikácie';
|
||||
@@ -1084,6 +1092,11 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Veľké verejné';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Osobné';
|
||||
|
||||
@@ -1226,6 +1239,14 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Odoslať správu';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošli správu $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Pošlite správu na začiatok';
|
||||
|
||||
@@ -1245,11 +1266,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Lokalita';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošli správu $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Napište správu...';
|
||||
|
||||
@@ -1442,6 +1458,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Úspechy';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Odstrániť cestu';
|
||||
|
||||
@@ -1543,6 +1562,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Nezriadené: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Otvoriť odkaz?';
|
||||
|
||||
@@ -1624,6 +1649,24 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Zástavy';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Zdieľte značku tu';
|
||||
|
||||
@@ -1709,6 +1752,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Zdieľaný PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Zdieľané';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pripojiť miestnosť';
|
||||
|
||||
@@ -2001,9 +2047,18 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Správa servera miestnosti';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informácie o opakovači';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informácie o serveri';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Nástroje na správu';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Nástroje pre hostí';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -2036,6 +2091,14 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Synchronizácia hodiniek po prihlávení';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status opakého zboru';
|
||||
|
||||
@@ -2406,6 +2469,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Hodiny';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Synchronizácia hodin';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Objaviť susedov';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.';
|
||||
|
||||
@@ -3190,6 +3259,37 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terén';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Zablokované miesta';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Vybraná prekážka';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvencia';
|
||||
|
||||
@@ -3545,6 +3645,77 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Úroveň hluku, RSSI, SNR a časové rozloženie';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Preklad';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivovať preklad';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Preložte pred odeslaním';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Riadi výchoce stav ikony pre preklad, ktorú používa program.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Cieľový jazyk';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Použite jazyk aplikácie';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Stiahnutý model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Prednastavený model od Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel =>
|
||||
'Odkaz na manuál (v elektronickej forme)';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Stiahnuť model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Stiahnutie...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Práca...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Zastavte';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Sliečenie stiahnutých častí do konečného súboru...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Stiahnuté modely';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Odstrániť model';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Model pre preklad bol stiahnutý.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Stiahnutie bolo prerušené.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Neúspešné stiahnutie: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Najprv zadajte URL pre konkrétny model.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Zobraziť PIN';
|
||||
|
||||
@@ -3552,10 +3723,71 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get scanner_linuxPairingHidePin => 'Skryť PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Bluetooth párovací PIN';
|
||||
String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Zadajte PIN pre $deviceName (ak nie je, nechajte prázdne).';
|
||||
return 'Zadajte PIN pre $deviceName (ak neexistuje, nechajte prázdne).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Preklad textu';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Preložte pred odeslaním';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Správy budú preložené, než budú odoslané.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Posielajte správy v pôvodnej písanom jazyku.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Preložte do $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Možnosti prekladania';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Jazyk systému';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Zapri';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Uredi';
|
||||
|
||||
@@ -430,9 +433,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Večkratni potrditvi: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Več potrdil';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
|
||||
@@ -440,6 +441,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Akcije';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Pošlji Oglas';
|
||||
|
||||
@@ -480,11 +488,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get settings_debug => 'Debug';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE debug log (razhroščevanje)';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle =>
|
||||
'BLE ukazi, odgovori in surovi podatki';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Logi aplikacije';
|
||||
@@ -1082,6 +1090,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Javni';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Zasebni';
|
||||
|
||||
@@ -1224,6 +1237,14 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Še ni sporočil.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Pošlji sporočilo';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošlji sporočilo $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.';
|
||||
|
||||
@@ -1244,11 +1265,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Lokacija';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošlji sporočilo $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Vnesi sporočilo...';
|
||||
|
||||
@@ -1439,6 +1455,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'Uspešni';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Izbriši pot';
|
||||
|
||||
@@ -1538,6 +1557,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Nerešeno: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Odpreti povezavo?';
|
||||
|
||||
@@ -1620,6 +1645,24 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Zapestnice';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Delite točke tukaj.';
|
||||
|
||||
@@ -1704,6 +1747,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Deljeno naslovno geslo';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Deljeno';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Pridružiti sobo';
|
||||
|
||||
@@ -1998,9 +2044,18 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Upravljanje stremlišča';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informacije o ponovljalniku';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informacije o strežniku';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Upravne orodje';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Naložila za goste';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -2035,6 +2090,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Konfigurirajte parametre ponovitelja';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => 'Sinhronizacija ure po prijavi';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status ponovitelja';
|
||||
|
||||
@@ -2409,6 +2471,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Ura';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Usklajevanje ure';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Odkrijte sosede';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Pošlje paket oglasov';
|
||||
|
||||
@@ -3192,6 +3260,37 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Teren';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Zasedena parkirišča';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Dotaknite blokirano točko, da jo označite na zemljeplati.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Izbrano ovire';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvenca';
|
||||
|
||||
@@ -3550,6 +3649,76 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Prevod';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Omogočite prevod';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Prevedite vstopne sporočila in omogočite predhodno prevajanje.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Preprištejte, preden pošljete';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Ciljna jezika';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Uporabite jezik aplikacije';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Naložen model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Prednastavljeni model Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL za ročni model';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Prenesite model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Izvajanje...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Delo...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Prekliji';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Sklapljanje prenesenih delov v končni datoteko...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Naloženi modeli';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Izbrisati model';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded =>
|
||||
'Model za prevajanje je bil naložen.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Prenos je bil prekinjen.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Izgovoritev ni bila uspešna: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Najprej vnesite URL model.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Prikaži PIN';
|
||||
|
||||
@@ -3563,4 +3732,66 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Vnesite PIN za $deviceName (pustite prazno, če ga ni).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Prevod sporočila';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Preprištejte, preden pošljete';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Vsebina sporočil bo prevedena, preden jih pošljemo.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Pošljite sporočila v originalnem tipkanem jeziku.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Prevesti v $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Možnosti prevoda';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Jezik sistema';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => 'Stänga';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => 'Redigera';
|
||||
|
||||
@@ -428,9 +431,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
String get settings_multiAck => 'Flera bekräftelser';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
|
||||
@@ -438,6 +439,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => 'Åtgärder';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => 'Skicka Annons';
|
||||
|
||||
@@ -477,10 +485,11 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_debug => 'Felsök';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE-felsökning';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle => 'BLE-kommandon, svar och rådata';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => 'Appfelsökning';
|
||||
@@ -1075,6 +1084,11 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => 'Offentligt';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => 'Privat';
|
||||
|
||||
@@ -1217,6 +1231,14 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Inga meddelanden ännu';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Skicka meddelande';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Skicka ett meddelande till $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart =>
|
||||
'Skicka ett meddelande för att komma igång';
|
||||
@@ -1238,11 +1260,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Plats';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Skicka ett meddelande till $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Skriv ett meddelande...';
|
||||
|
||||
@@ -1434,6 +1451,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => 'framgångar';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => 'Ta bort sökväg';
|
||||
|
||||
@@ -1532,6 +1552,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Olästa: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Öppna länk?';
|
||||
|
||||
@@ -1613,6 +1639,24 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => 'Flaggor';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Dela markeringen här';
|
||||
|
||||
@@ -1698,6 +1742,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => 'Delad PIN';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => 'Delad';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Gå med i rum';
|
||||
|
||||
@@ -1987,9 +2034,18 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Rumserverhantering';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Information om repetorer';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Information om servern';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Administrationsverktyg';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Gästverktyg';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -2022,6 +2078,14 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Synkronisera klockan efter inloggning';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Återspelsstatus';
|
||||
|
||||
@@ -2394,6 +2458,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Klocka';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Synkronisera klocka';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Upptäck grannar';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Skickar ett annonspaket';
|
||||
|
||||
@@ -3173,6 +3243,37 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => 'Terräng';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => 'Reserverade platser';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint =>
|
||||
'Klicka på en markerad plats för att framhäva den på kartan.';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => 'Vald hinder';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frekvens';
|
||||
|
||||
@@ -3527,6 +3628,77 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Bakgrundsnivå, RSSI, SNR och tillgänglig tid';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Översättning';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivera översättning';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Översätt inkommande meddelanden och möjliggör översättning före avsändning.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Översätt innan du skickar';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Styr standardtillståndet för kompositorns översättningsikon.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Målmedvetet språk';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Använd appens språk';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Nedladdad modell';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Fördefinierat Hugging Face-modell';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Manualens URL';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Ladda ner modellen';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Nedladdning...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Arbeta...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stopp';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Slå samman de nedladdade delarna till en slutlig fil...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Nedladdade modeller';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Ta bort modell';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded =>
|
||||
'Översättningsmodellen har laddats ner.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Nedladdningen avbruten.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Nedladdning misslyckades: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Ange först en URL för en specifik modell.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Visa PIN';
|
||||
|
||||
@@ -3540,4 +3712,65 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Ange PIN för $deviceName (lämna tomt om ingen).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Meddelandets översättning';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Översätt innan du skickar';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Meddelandena kommer att översättas innan de skickas.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Skicka meddelanden på det ursprungliga, stavade språket.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Översätt till $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Översättningsalternativ';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Språk för systemet';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
+423
-194
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get common_close => '关闭';
|
||||
|
||||
@override
|
||||
String get common_done => 'Done';
|
||||
|
||||
@override
|
||||
String get common_edit => '编辑';
|
||||
|
||||
@@ -408,9 +411,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle => '在广告中包含位置';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '多重ACK:$value';
|
||||
}
|
||||
String get settings_multiAck => '多重ACK';
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => '遥测模式已更新';
|
||||
@@ -418,6 +419,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_actions => '操作';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPaths => 'Delete All Paths';
|
||||
|
||||
@override
|
||||
String get settings_deleteAllPathsSubtitle =>
|
||||
'Clear all path data from contacts.';
|
||||
|
||||
@override
|
||||
String get settings_sendAdvertisement => '发送广播';
|
||||
|
||||
@@ -455,10 +463,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get settings_debug => '调试';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLog => 'BLE 调试日志';
|
||||
String get settings_companionDebugLog => 'Companion Debug Log';
|
||||
|
||||
@override
|
||||
String get settings_bleDebugLogSubtitle => 'BLE 命令、响应和原始数据';
|
||||
String get settings_companionDebugLogSubtitle =>
|
||||
'BLE/TCP/USB commands, responses, and raw data';
|
||||
|
||||
@override
|
||||
String get settings_appDebugLog => '应用调试日志';
|
||||
@@ -1023,6 +1032,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get channels_public => '公共';
|
||||
|
||||
@override
|
||||
String channels_via(String path) {
|
||||
return 'via $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_private => '私有';
|
||||
|
||||
@@ -1161,6 +1175,14 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => '暂无消息';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => '发送消息';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '发送消息给 $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => '发送消息开始对话';
|
||||
|
||||
@@ -1180,11 +1202,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => '位置';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '发送消息给 $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => '输入消息...';
|
||||
|
||||
@@ -1367,6 +1384,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_successes => '成功';
|
||||
|
||||
@override
|
||||
String get chat_score => 'Score';
|
||||
|
||||
@override
|
||||
String get chat_removePath => '移除路径';
|
||||
|
||||
@@ -1454,6 +1474,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '未读:$count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_markAsUnread => 'Mark as Unread';
|
||||
|
||||
@override
|
||||
String get chat_newMessages => 'New messages';
|
||||
|
||||
@override
|
||||
String get chat_openLink => '打开链接?';
|
||||
|
||||
@@ -1532,6 +1558,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_flags => '标志';
|
||||
|
||||
@override
|
||||
String get map_type => 'Type';
|
||||
|
||||
@override
|
||||
String get map_path => 'Path';
|
||||
|
||||
@override
|
||||
String get map_location => 'Location';
|
||||
|
||||
@override
|
||||
String get map_estLocation => 'Est. Location';
|
||||
|
||||
@override
|
||||
String get map_publicKey => 'Public Key';
|
||||
|
||||
@override
|
||||
String get map_publicKeyPrefixHint => 'e.g. ab12';
|
||||
|
||||
@override
|
||||
String get map_shareMarkerHere => '在此分享标记';
|
||||
|
||||
@@ -1615,6 +1659,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_sharedPin => '共享标记';
|
||||
|
||||
@override
|
||||
String get map_sharedAt => '已分享';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => '加入房间';
|
||||
|
||||
@@ -1887,9 +1934,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => '房间服务器管理';
|
||||
|
||||
@override
|
||||
String get repeater_guest => '重复器信息';
|
||||
|
||||
@override
|
||||
String get room_guest => '服务器信息';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => '管理工具';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => '访客工具';
|
||||
|
||||
@override
|
||||
String get repeater_status => '状态';
|
||||
|
||||
@@ -1920,6 +1976,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => '配置转发节点参数';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => '登录后,自动同步时钟';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle => '在成功登录后,自动发送“时钟同步”指令。';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => '转发节点状态';
|
||||
|
||||
@@ -2277,6 +2339,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => '时钟';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => '同步时钟';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => '发现邻居';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => '发送广播包';
|
||||
|
||||
@@ -2992,6 +3060,36 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get losLegendTerrain => '地形';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsTitle => '被占用区域';
|
||||
|
||||
@override
|
||||
String get losBlockedSpotsHint => '点击地图上的某个被遮盖的区域,以突出显示该区域。';
|
||||
|
||||
@override
|
||||
String losBlockedSpotChip(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit • $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losSelectedObstructionTitle => '选择性阻碍';
|
||||
|
||||
@override
|
||||
String losSelectedObstructionDetails(
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
String distanceFromA,
|
||||
String distanceUnit,
|
||||
String distanceFromB,
|
||||
) {
|
||||
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => '频率';
|
||||
|
||||
@@ -3300,7 +3398,72 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get radioStats_settingsSubtitle => '噪声水平、RSSI、信噪比和空中时间';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => '显示 PIN码';
|
||||
String get translation_title => '翻译';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => '启用翻译功能';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle => '翻译收到的消息,并允许在发送前进行翻译。';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => '在发送之前进行翻译';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle => '控制作曲家翻译图标的默认状态。';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => '目标语言';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => '使用应用程序语言';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => '下载的模型';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => '预设的 Hugging Face 模型';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => '手动模型网址';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => '下载模型';
|
||||
|
||||
@override
|
||||
String get translation_downloading => '正在下载...';
|
||||
|
||||
@override
|
||||
String get translation_working => '工作中...';
|
||||
|
||||
@override
|
||||
String get translation_stop => '停止';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks => '将下载的片段合并成最终文件...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => '下载的模型';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => '删除模型';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => '翻译模型已下载。';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => '下载已停止。';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return '下载失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => '首先,请输入模型的 URL。';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => '显示PIN码';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => '隐藏 PIN';
|
||||
@@ -3310,6 +3473,65 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return '输入 $deviceName 的 PIN(如果没有,请留空)。';
|
||||
return '输入 $deviceName 的 PIN 码(如果为空,则留空)。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => '消息翻译';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => '在发送前进行翻译';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint => '消息将在发送前进行翻译。';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint => '使用原始的打字方式发送消息。';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return '翻译成 $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => '翻译选项';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => '系统语言';
|
||||
|
||||
@override
|
||||
String get background_serviceTitle => 'MeshCore running';
|
||||
|
||||
@override
|
||||
String get background_serviceText => 'Keeping BLE connected';
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleted(String name) {
|
||||
return 'Deleted $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String appSettings_translationModelDeleteFailed(String error) {
|
||||
return 'Failed to delete: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelUpdateFailed(String error) {
|
||||
return 'Failed to update channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_typeChat => 'Chat';
|
||||
|
||||
@override
|
||||
String get contact_typeRepeater => 'Repeater';
|
||||
|
||||
@override
|
||||
String get contact_typeRoom => 'Room';
|
||||
|
||||
@override
|
||||
String get contact_typeSensor => 'Sensor';
|
||||
|
||||
@override
|
||||
String get contact_typeUnknown => 'Unknown';
|
||||
}
|
||||
|
||||
+187
-55
@@ -84,7 +84,7 @@
|
||||
"settings_appSettings": "App Instellingen",
|
||||
"settings_appSettingsSubtitle": "Notificaties, berichten en kaartinstellingen",
|
||||
"settings_nodeSettings": "Node Instellingen",
|
||||
"settings_nodeName": "Node Naam",
|
||||
"settings_nodeName": "Nodenaam",
|
||||
"settings_nodeNameNotSet": "Niet ingesteld",
|
||||
"settings_nodeNameHint": "Voer nodenaam in",
|
||||
"settings_nodeNameUpdated": "Naam bijgewerkt",
|
||||
@@ -104,16 +104,18 @@
|
||||
"settings_privacyModeEnabled": "Privacy modus is ingeschakeld",
|
||||
"settings_privacyModeDisabled": "Privacy modus is uitgeschakeld",
|
||||
"settings_actions": "Acties",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Verzend Advertentie",
|
||||
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
|
||||
"settings_advertisementSent": "Advertentie verzonden",
|
||||
"settings_syncTime": "Synchronisatie Tijd",
|
||||
"settings_syncTime": "Tijd Synchroniseren",
|
||||
"settings_syncTimeSubtitle": "Stel de apparaatklok in op de tijd van de telefoon.",
|
||||
"settings_timeSynchronized": "Tijdsynchronisatie",
|
||||
"settings_refreshContacts": "Contacten vernieuwen",
|
||||
"settings_refreshContactsSubtitle": "Contactlijst opnieuw laden van het apparaat",
|
||||
"settings_rebootDevice": "Apparaat opnieuw opstarten",
|
||||
"settings_rebootDeviceSubtitle": "Herstart het MeshCore apparaat",
|
||||
"settings_rebootDeviceSubtitle": "Herstart het MeshCore-apparaat",
|
||||
"settings_rebootDeviceConfirm": "Ben je er zeker van dat je het apparaat opnieuw wilt opstarten? Je wordt losgekoppeld.",
|
||||
"settings_debug": "Debug",
|
||||
"settings_bleDebugLog": "BLE Debug Log",
|
||||
@@ -145,7 +147,7 @@
|
||||
"settings_bandwidth": "Bandbreedte",
|
||||
"settings_spreadingFactor": "Spreadsnelheid",
|
||||
"settings_codingRate": "Codeertarief",
|
||||
"settings_txPower": "TX Vermogen (dBm)",
|
||||
"settings_txPower": "TX-Vermogen (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
||||
"settings_error": "Fout: {message}",
|
||||
@@ -232,11 +234,11 @@
|
||||
"appSettings_mapTimeFilter": "Filter tijd op kaart",
|
||||
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
|
||||
"appSettings_allTime": "Altijd",
|
||||
"appSettings_lastHour": "Laat uur",
|
||||
"appSettings_last6Hours": "laatste 6 uur",
|
||||
"appSettings_last24Hours": "De laatste 24 uur",
|
||||
"appSettings_lastWeek": "Laatste week",
|
||||
"appSettings_offlineMapCache": "Offline Kaarten Cache",
|
||||
"appSettings_lastHour": "Afgelopen uur",
|
||||
"appSettings_last6Hours": "Afgelopen 6 uur",
|
||||
"appSettings_last24Hours": "Afgelopen 24 uur",
|
||||
"appSettings_lastWeek": "Afgelopen week",
|
||||
"appSettings_offlineMapCache": "Offline Kaartcache",
|
||||
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
|
||||
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
|
||||
"@appSettings_areaSelectedZoom": {
|
||||
@@ -682,7 +684,7 @@
|
||||
"map_showSharedMarkers": "Toon gedeelde markeringen",
|
||||
"map_lastSeenTime": "Laatste Bekeken Tijd",
|
||||
"map_sharedPin": "Gedeelde pin",
|
||||
"map_joinRoom": "Sluit Kamer",
|
||||
"map_joinRoom": "Kamer Toetreden",
|
||||
"map_manageRepeater": "Beheer Repeater",
|
||||
"mapCache_title": "Offline Kaarten Cache",
|
||||
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
|
||||
@@ -878,7 +880,7 @@
|
||||
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
|
||||
"path_setPath": "Stel Pad in",
|
||||
"repeater_management": "Beheer Repeaters",
|
||||
"repeater_managementTools": "Beheerinstrumenten",
|
||||
"repeater_managementTools": "Beheerfuncties",
|
||||
"repeater_status": "Status",
|
||||
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
|
||||
"repeater_telemetry": "Telemetry",
|
||||
@@ -912,8 +914,8 @@
|
||||
"repeater_lastRssi": "Laatste RSSI",
|
||||
"repeater_lastSnr": "Laatste SNR",
|
||||
"repeater_noiseFloor": "Ruisvloer",
|
||||
"repeater_txAirtime": "TX Airtime",
|
||||
"repeater_rxAirtime": "RX Airtime",
|
||||
"repeater_txAirtime": "TX-zendtijd",
|
||||
"repeater_rxAirtime": "RX-zendtijd",
|
||||
"repeater_packetStatistics": "Pakketstatistieken",
|
||||
"repeater_sent": "Verzonden",
|
||||
"repeater_received": "Ontvangen",
|
||||
@@ -982,14 +984,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsTitle": "Repeater Instellingen",
|
||||
"repeater_settingsTitle": "Repeaterinstellingen",
|
||||
"repeater_basicSettings": "Basisinstellingen",
|
||||
"repeater_repeaterName": "Repeaternaam",
|
||||
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
|
||||
"repeater_repeaterNameHelper": "Weergavenaam voor deze repeater",
|
||||
"repeater_adminPassword": "Admin wachtwoord",
|
||||
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
|
||||
"repeater_guestPassword": "Wachtwoord Gast",
|
||||
"repeater_guestPasswordHelper": "Leesbeheer wachtwoord",
|
||||
"repeater_adminPasswordHelper": "Wachtwoord administratortoegang",
|
||||
"repeater_guestPassword": "Gast wachtwoord",
|
||||
"repeater_guestPasswordHelper": "Wachtwoord gasttoegen",
|
||||
"repeater_radioSettings": "Radio Instellingen",
|
||||
"repeater_frequencyMhz": "Frequentie (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
@@ -998,7 +1000,7 @@
|
||||
"repeater_bandwidth": "Bandbreedte",
|
||||
"repeater_spreadingFactor": "Spreidingsfactor",
|
||||
"repeater_codingRate": "Codeertarief",
|
||||
"repeater_locationSettings": "Locatie Instellingen",
|
||||
"repeater_locationSettings": "Locatie-instellingen",
|
||||
"repeater_latitude": "Breedtegraad",
|
||||
"repeater_latitudeHelper": "Graadseconden (bijv. 37.7749)",
|
||||
"repeater_longitude": "Lengtegraad",
|
||||
@@ -1008,9 +1010,9 @@
|
||||
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
|
||||
"repeater_guestAccess": "Toegang voor Gasten",
|
||||
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
|
||||
"repeater_privacyMode": "Privacy Modus",
|
||||
"repeater_privacyMode": "Privacymodus",
|
||||
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
|
||||
"repeater_advertisementSettings": "Advertentie Instellingen",
|
||||
"repeater_advertisementSettings": "Advertentie-instellingen",
|
||||
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
|
||||
"repeater_localAdvertIntervalMinutes": "{minutes} minuten",
|
||||
"@repeater_localAdvertIntervalMinutes": {
|
||||
@@ -1073,7 +1075,7 @@
|
||||
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
|
||||
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
|
||||
"repeater_refreshGuestAccess": "Toegang Gast Vernieuwen",
|
||||
"repeater_refreshPrivacyMode": "Privacy Mode vernieuwen",
|
||||
"repeater_refreshPrivacyMode": "Privacymode vernieuwen",
|
||||
"repeater_refreshAdvertisementSettings": "Instellingen Advertentie Bijwerken",
|
||||
"repeater_refreshed": "{label} is vernieuwd",
|
||||
"@repeater_refreshed": {
|
||||
@@ -1094,8 +1096,8 @@
|
||||
"repeater_cliTitle": "Repeater CLI",
|
||||
"repeater_debugNextCommand": "Debug Volgende Commando",
|
||||
"repeater_commandHelp": "Help",
|
||||
"repeater_clearHistory": "Verwijder Geschiedenis",
|
||||
"repeater_noCommandsSent": "Geen commando's verzonden nog.",
|
||||
"repeater_clearHistory": "Geschiedenis Verwijderen",
|
||||
"repeater_noCommandsSent": "Nog geen commando's verzonden.",
|
||||
"repeater_typeCommandOrUseQuick": "Typ een opdracht hieronder of gebruik snelle commando's",
|
||||
"repeater_enterCommandHint": "Voer bevel in...",
|
||||
"repeater_previousCommand": "Vorige opdracht",
|
||||
@@ -1110,14 +1112,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_cliQuickGetName": "Haal Naam op",
|
||||
"repeater_cliQuickGetRadio": "Radio ontvangen",
|
||||
"repeater_cliQuickGetTx": "Krijg TX",
|
||||
"repeater_cliQuickNeighbors": "Buren",
|
||||
"repeater_cliQuickVersion": "Versie",
|
||||
"repeater_cliQuickAdvertise": "Advertenties",
|
||||
"repeater_cliQuickClock": "Tijd",
|
||||
"repeater_cliHelpAdvert": "Verstuurt een advertentiepakket",
|
||||
"repeater_cliQuickGetName": "Naam opvragen",
|
||||
"repeater_cliQuickGetRadio": "Radio-instellingen opvragen",
|
||||
"repeater_cliQuickGetTx": "TX opvragen",
|
||||
"repeater_cliQuickNeighbors": "Buren opvragen",
|
||||
"repeater_cliQuickVersion": "Versie opvragen",
|
||||
"repeater_cliQuickAdvertise": "Advertenties opvragen",
|
||||
"repeater_cliQuickClock": "Tijd opvragen",
|
||||
"repeater_cliHelpAdvert": "Advertentie uitzenden",
|
||||
"repeater_cliHelpReboot": "Herstart het apparaat. (let op, je krijgt mogelijk een 'Timeout', wat normaal is)",
|
||||
"repeater_cliHelpClock": "Toont de huidige tijd per apparaat's klok.",
|
||||
"repeater_cliHelpPassword": "Stelt een nieuw beheerderswachtwoord in voor het apparaat.",
|
||||
@@ -1203,7 +1205,7 @@
|
||||
},
|
||||
"telemetry_batteryLabel": "Batterij",
|
||||
"telemetry_voltageLabel": "Spanning",
|
||||
"telemetry_mcuTemperatureLabel": "MCU Temperatuur",
|
||||
"telemetry_mcuTemperatureLabel": "MCU-temperatuur",
|
||||
"telemetry_temperatureLabel": "Temperatuur",
|
||||
"telemetry_currentLabel": "Huidig",
|
||||
"telemetry_batteryValue": "{percent}% / {volts}V",
|
||||
@@ -1346,7 +1348,7 @@
|
||||
"listFilter_tooltip": "Filteren en sorteren",
|
||||
"listFilter_sortBy": "Sorteren door",
|
||||
"listFilter_latestMessages": "Recente berichten",
|
||||
"listFilter_heardRecently": "Hoor je onlangs",
|
||||
"listFilter_heardRecently": "Recent gezien",
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_filters": "Filters",
|
||||
"listFilter_all": "Alles",
|
||||
@@ -1363,20 +1365,20 @@
|
||||
}
|
||||
},
|
||||
"repeater_neighbors": "Buren",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul-hopsburen.",
|
||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||
"neighbors_repeatersNeighbors": "Repeatbburen",
|
||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
|
||||
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
|
||||
"channels_joinPublicChannel": "Sluit het Open Kanaal",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
|
||||
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
|
||||
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
|
||||
"channels_createPrivateChannel": "PrivéKanaal Aanmaken",
|
||||
"channels_joinPrivateChannel": "PrivéKanaal Toetreden",
|
||||
"channels_joinPrivateChannelDesc": "Voer handmatig een geheime sleutel in.",
|
||||
"channels_joinPublicChannel": "Publiek Kanaal Toetreden",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan toetreden tot dit kanaal.",
|
||||
"channels_joinHashtagChannel": "Hashtag-kanaal Aanmaken",
|
||||
"channels_joinHashtagChannelDesc": "Iedereen kan toetreden tot hashtag-kanalen.",
|
||||
"channels_scanQrCode": "Scan een QR-code",
|
||||
"channels_scanQrCodeComingSoon": "Komt later",
|
||||
"channels_enterHashtag": "Voer hashtag in",
|
||||
@@ -1574,7 +1576,7 @@
|
||||
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
||||
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
||||
"contacts_ShareContact": "Contact naar Klembord kopiëren",
|
||||
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
||||
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||
"notification_activityTitle": "MeshCore Activiteit",
|
||||
@@ -1612,8 +1614,8 @@
|
||||
"snrIndicator_lastSeen": "Laatst gezien",
|
||||
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||
"chat_ShowAllPaths": "Toon alle paden",
|
||||
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
||||
"settings_clientRepeat": "Off-Grid Herhalen",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de berichten van andere apparaten doorsturen.",
|
||||
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
|
||||
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Eenheden",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_lastSeen": "Laatst gezien",
|
||||
"contact_clearChat": "Chat leegmaken",
|
||||
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.",
|
||||
"appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route",
|
||||
"appSettings_maxRouteWeight": "Maximale gewicht voor de route",
|
||||
@@ -1941,7 +1936,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Herhalingssleutel overlapt",
|
||||
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2007,6 +2001,34 @@
|
||||
"radioStats_stripWaiting": "Radio-statistieken ophalen…",
|
||||
"radioStats_settingsTile": "Statistieken over radio",
|
||||
"radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.",
|
||||
"translation_enableTitle": "Activeer vertaling",
|
||||
"translation_title": "Vertaling",
|
||||
"translation_composerTitle": "Vertaal voor verzending",
|
||||
"translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.",
|
||||
"translation_useAppLanguage": "Gebruik de taal van de app",
|
||||
"translation_targetLanguage": "Doeltaal",
|
||||
"translation_downloadedModelLabel": "Gedownloade model",
|
||||
"translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model",
|
||||
"translation_manualUrlLabel": "URL van de handleiding",
|
||||
"translation_downloadModel": "Download het model",
|
||||
"translation_downloading": "Downloaden...",
|
||||
"translation_working": "Werken...",
|
||||
"translation_mergingChunks": "Het samenvoegen van de gedownloade stukken tot één eindbestand...",
|
||||
"translation_stop": "Stoppen",
|
||||
"translation_downloadedModels": "Gedownloade modellen",
|
||||
"translation_deleteModel": "Model verwijderen",
|
||||
"translation_modelDownloaded": "Vertalingmodel gedownload.",
|
||||
"translation_downloadStopped": "Download is afgebroken.",
|
||||
"translation_downloadFailed": "Download mislukt: {error}",
|
||||
"translation_enterUrlFirst": "Voer eerst een URL van een model in.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2036,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Stuur berichten in de oorspronkelijke, getypte taal.",
|
||||
"translation_translateBeforeSending": "Vertaal voor verzending",
|
||||
"translation_composerEnabledHint": "De berichten worden vertaald voordat ze verzonden worden.",
|
||||
"translation_messageTranslation": "Berichtvertaling",
|
||||
"translation_translationOptions": "Opties voor vertaling",
|
||||
"translation_systemLanguage": "Taal van het systeem",
|
||||
"translation_translateTo": "Vertalen naar {language}",
|
||||
"scanner_linuxPairingShowPin": "Toon PIN",
|
||||
"scanner_linuxPairingHidePin": "PIN verbergen",
|
||||
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN"
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN",
|
||||
"repeater_cliQuickDiscovery": "Ontdek Buren",
|
||||
"repeater_cliQuickClockSync": "Kloksynchronisatie",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.",
|
||||
"repeater_clockSyncAfterLogin": "Na het inloggen, klok synchroniseren",
|
||||
"repeater_guestTools": "Gastenfuncties",
|
||||
"room_guest": "Informatie over de server",
|
||||
"chat_sendMessage": "Verzend bericht",
|
||||
"repeater_guest": "Informatie over herhalingsapparatuur",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Meerdere bevestigingen",
|
||||
"map_sharedAt": "Gedeeld",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Geselecteerde obstakel",
|
||||
"losBlockedSpotsHint": "Tik op een geblokkeerd gebied om het op de kaart te markeren.",
|
||||
"losBlockedSpotsTitle": "Geplande plaatsen",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+151
-19
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Tryb prywatności włączony",
|
||||
"settings_privacyModeDisabled": "Tryb prywatności wyłączony",
|
||||
"settings_actions": "Działania",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Wyślij rozgłoszenie",
|
||||
"settings_sendAdvertisementSubtitle": "Nadaj obecność teraz",
|
||||
"settings_advertisementSent": "Rozgłoszenie wysłane",
|
||||
@@ -163,7 +165,7 @@
|
||||
"appSettings_themeLight": "Jasne",
|
||||
"appSettings_themeDark": "Ciemny",
|
||||
"appSettings_language": "Język",
|
||||
"appSettings_languageSystem": "Domyślny systemu",
|
||||
"appSettings_languageSystem": "Domyślny systemowy",
|
||||
"appSettings_languageEn": "English",
|
||||
"appSettings_languageFr": "Français",
|
||||
"appSettings_languageEs": "Español",
|
||||
@@ -1373,7 +1375,7 @@
|
||||
}
|
||||
},
|
||||
"repeater_neighbors": "Sąsiedzi",
|
||||
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||
"repeater_neighborsSubtitle": "Wyświetl sąsiadów zero-hop.",
|
||||
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
||||
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
||||
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
||||
@@ -1622,12 +1624,12 @@
|
||||
},
|
||||
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
|
||||
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
|
||||
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / serwer pokojowy do GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / roomservery do GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.",
|
||||
"settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym",
|
||||
"settings_gpxExportError": "Wystąpił błąd podczas eksportowania.",
|
||||
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i serwerów pokojowych",
|
||||
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i roomserverów",
|
||||
"settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX",
|
||||
"settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.",
|
||||
@@ -1648,7 +1650,7 @@
|
||||
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
||||
"scanner_enableBluetooth": "Włącz Bluetooth",
|
||||
"snrIndicator_lastSeen": "Ostatnio widziany",
|
||||
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
|
||||
"snrIndicator_nearByRepeaters": "Pobliskie przekaźniki",
|
||||
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
|
||||
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
|
||||
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
|
||||
@@ -1846,12 +1848,12 @@
|
||||
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie przekaźników",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie odkrytych przekaźników.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj roomservery",
|
||||
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
|
||||
"settings_contactSettings": "Ustawienia kontaktów",
|
||||
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
|
||||
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
|
||||
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
|
||||
"discoveredContacts_contactAdded": "Kontakt dodany",
|
||||
@@ -1960,13 +1962,6 @@
|
||||
"contact_settings": "Ustawienia kontaktowe",
|
||||
"contact_lastSeen": "Ostatnio widziany",
|
||||
"contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Początkowa waga trasy",
|
||||
"appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu",
|
||||
"appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek",
|
||||
@@ -1979,9 +1974,8 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
|
||||
"settings_multiAck": "Wiele potwierdzeń: {value}",
|
||||
"map_showOverlaps": "Nakładające się klucze powtarzalne",
|
||||
"map_runTraceWithReturnPath": "Wróć z powrotem tą samą ścieżką",
|
||||
"map_showOverlaps": "Nakładające się klucze przekaźników",
|
||||
"map_runTraceWithReturnPath": "Wróć tą samą ścieżką",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
@@ -2045,6 +2039,34 @@
|
||||
"radioStats_stripWaiting": "Pobieranie danych dotyczących radia…",
|
||||
"radioStats_settingsTile": "Statystyki radiowe",
|
||||
"radioStats_settingsSubtitle": "Szum tła, RSSI, SNR oraz czas dostępny",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Przekład przed wysłaniem",
|
||||
"translation_title": "Tłumaczenie",
|
||||
"translation_enableTitle": "Włącz tłumaczenie",
|
||||
"translation_enableSubtitle": "Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.",
|
||||
"translation_composerSubtitle": "Kontroluje domyślny stan ikony tłumaczenia w edytorze.",
|
||||
"translation_targetLanguage": "Język docelowy",
|
||||
"translation_useAppLanguage": "Użyj języka aplikacji",
|
||||
"translation_downloadedModelLabel": "Pobudowany model",
|
||||
"translation_presetModelLabel": "Wspólny model Hugging Face",
|
||||
"translation_manualUrlLabel": "Adres URL do wersji manualnej",
|
||||
"translation_downloadModel": "Pobierz model",
|
||||
"translation_downloading": "Pobieranie...",
|
||||
"translation_working": "Praca...",
|
||||
"translation_stop": "Zatrzymaj się",
|
||||
"translation_mergingChunks": "Scalanie pobranych fragmentów w jeden plik końcowy...",
|
||||
"translation_downloadedModels": "Pobrane modele",
|
||||
"translation_deleteModel": "Usuń model",
|
||||
"translation_modelDownloaded": "Model tłumaczenia został pobrany.",
|
||||
"translation_downloadStopped": "Pobieranie zakończone.",
|
||||
"translation_downloadFailed": "Nie udało się pobrać: {error}",
|
||||
"translation_enterUrlFirst": "Najpierw wprowadź adres URL modelu.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2052,8 +2074,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerEnabledHint": "Komunikaty zostaną przetłumaczone przed wysłaniem.",
|
||||
"translation_translateBeforeSending": "Przekład przed wysłaniem",
|
||||
"translation_composerDisabledHint": "Wysyłaj wiadomości w oryginalnym, wpisanym formacie.",
|
||||
"translation_messageTranslation": "Tłumaczenie wiadomości",
|
||||
"translation_translationOptions": "Opcje tłumaczenia",
|
||||
"translation_systemLanguage": "Język systemu",
|
||||
"translation_translateTo": "Tłumacz na {language}",
|
||||
"scanner_linuxPairingShowPin": "Pokaż PIN",
|
||||
"scanner_linuxPairingHidePin": "Ukryj PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).",
|
||||
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth"
|
||||
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth",
|
||||
"repeater_cliQuickClockSync": "Synchronizacja zegara",
|
||||
"repeater_cliQuickDiscovery": "Odkryj Sąsiadów",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Synchronizacja zegara po zalogowaniu",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.",
|
||||
"chat_sendMessage": "Wyślij wiadomość",
|
||||
"repeater_guestTools": "Narzędzia dla gości",
|
||||
"repeater_guest": "Informacje dotyczące urządzenia powtarzającego",
|
||||
"room_guest": "Informacje o serwerze",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Wielokrotne potwierdzenia odbioru",
|
||||
"map_sharedAt": "Udostępnione",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losSelectedObstructionTitle": "Wybór przeszkody",
|
||||
"losBlockedSpotsTitle": "Zablokowane miejsca",
|
||||
"losBlockedSpotsHint": "Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+141
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Modo de privacidade ativado",
|
||||
"settings_privacyModeDisabled": "Modo de privacidade desativado",
|
||||
"settings_actions": "Ações",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Enviar Publicidade",
|
||||
"settings_sendAdvertisementSubtitle": "Presença de transmissão agora",
|
||||
"settings_advertisementSent": "Anúncio enviado",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_settings": "Configurações de Contato",
|
||||
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso Inicial da Rota",
|
||||
"appSettings_maxRouteWeight": "Peso Máximo da Rota",
|
||||
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
|
||||
@@ -1941,7 +1936,7 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"settings_multiAck": "Multi-ACKs",
|
||||
"map_showOverlaps": "Sobreposições da Chave Repeater",
|
||||
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2007,6 +2002,34 @@
|
||||
"radioStats_stripWaiting": "Obtendo estatísticas de rádio…",
|
||||
"radioStats_settingsTile": "Estatísticas de rádio",
|
||||
"radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Traduza antes de enviar",
|
||||
"translation_enableSubtitle": "Traduzir mensagens recebidas e permitir a tradução antes do envio.",
|
||||
"translation_enableTitle": "Ativar a tradução",
|
||||
"translation_title": "Tradução",
|
||||
"translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.",
|
||||
"translation_targetLanguage": "Língua-alvo",
|
||||
"translation_useAppLanguage": "Utilize o idioma da aplicação",
|
||||
"translation_downloadedModelLabel": "Modelo baixado",
|
||||
"translation_presetModelLabel": "Modelo pré-definido da Hugging Face",
|
||||
"translation_manualUrlLabel": "URL do modelo manual",
|
||||
"translation_downloading": "Baixando...",
|
||||
"translation_downloadModel": "Baixar modelo",
|
||||
"translation_working": "Trabalhando...",
|
||||
"translation_stop": "Pare",
|
||||
"translation_mergingChunks": "Combinando os fragmentos baixados em um único arquivo...",
|
||||
"translation_downloadedModels": "Modelos baixados",
|
||||
"translation_deleteModel": "Excluir modelo",
|
||||
"translation_modelDownloaded": "Modelo de tradução baixado.",
|
||||
"translation_downloadStopped": "Download interrompido.",
|
||||
"translation_downloadFailed": "Falha na descarga: {error}",
|
||||
"translation_enterUrlFirst": "Insira primeiro a URL do modelo.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2037,117 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Tradução da mensagem",
|
||||
"translation_translateBeforeSending": "Traduzir antes de enviar",
|
||||
"translation_composerEnabledHint": "As mensagens serão traduzidas antes de serem enviadas.",
|
||||
"translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.",
|
||||
"translation_translateTo": "Traduzir para {language}",
|
||||
"translation_translationOptions": "Opções de tradução",
|
||||
"translation_systemLanguage": "Idioma do sistema",
|
||||
"scanner_linuxPairingShowPin": "Mostrar PIN",
|
||||
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
|
||||
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth"
|
||||
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
|
||||
"repeater_cliQuickClockSync": "Sincronização do Relógio",
|
||||
"repeater_cliQuickDiscovery": "Descobrir Vizinhos",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.",
|
||||
"repeater_clockSyncAfterLogin": "Sincronização do relógio após o login",
|
||||
"room_guest": "Informações do Servidor",
|
||||
"chat_sendMessage": "Enviar mensagem",
|
||||
"repeater_guest": "Informações sobre repetidores",
|
||||
"repeater_guestTools": "Ferramentas para hóspedes",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "Compartilhado",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Locais ocupados",
|
||||
"losBlockedSpotsHint": "Toque em um ponto bloqueado para destacá-lo no mapa.",
|
||||
"losSelectedObstructionTitle": "Obstrução selecionada",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+141
-9
@@ -81,6 +81,8 @@
|
||||
"settings_privacyModeEnabled": "Режим конфиденциальности включен",
|
||||
"settings_privacyModeDisabled": "Режим конфиденциальности выключен",
|
||||
"settings_actions": "Действия",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Отправить анонсирование",
|
||||
"settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас",
|
||||
"settings_advertisementSent": "Анонсирование отправлено",
|
||||
@@ -1162,13 +1164,6 @@
|
||||
"contact_clearChat": "Очистить чат",
|
||||
"contact_lastSeen": "Последний раз видели",
|
||||
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
|
||||
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
|
||||
@@ -1181,7 +1176,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
|
||||
"settings_multiAck": "Мульти-ACK: {value}",
|
||||
"map_showOverlaps": "Перекрытия ключа повтора",
|
||||
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -1247,6 +1241,34 @@
|
||||
"radioStats_stripWaiting": "Получение данных о радио…",
|
||||
"radioStats_settingsTile": "Статистика радиовещания",
|
||||
"radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Переводить входящие сообщения и позволять предварительный перевод перед отправкой.",
|
||||
"translation_composerTitle": "Переводить перед отправкой",
|
||||
"translation_title": "Перевод",
|
||||
"translation_enableTitle": "Включить перевод",
|
||||
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
|
||||
"translation_targetLanguage": "Целевой язык",
|
||||
"translation_useAppLanguage": "Используйте язык приложения",
|
||||
"translation_downloadedModelLabel": "Загруженная модель",
|
||||
"translation_presetModelLabel": "Предопределенная модель от Hugging Face",
|
||||
"translation_manualUrlLabel": "Ссылка на руководство",
|
||||
"translation_downloadModel": "Скачать модель",
|
||||
"translation_downloading": "Загрузка...",
|
||||
"translation_stop": "Прекратите",
|
||||
"translation_working": "Работа...",
|
||||
"translation_mergingChunks": "Объединение скачанных фрагментов в один финальный файл...",
|
||||
"translation_downloadedModels": "Загруженные модели",
|
||||
"translation_deleteModel": "Удалить модель",
|
||||
"translation_modelDownloaded": "Модель перевода загружена.",
|
||||
"translation_downloadStopped": "Процесс загрузки был прерван.",
|
||||
"translation_downloadFailed": "Не удалось скачать: {error}",
|
||||
"translation_enterUrlFirst": "Сначала введите URL модели.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -1254,8 +1276,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "Перевести перед отправкой",
|
||||
"translation_composerEnabledHint": "Сообщения будут переведены перед отправкой.",
|
||||
"translation_messageTranslation": "Перевод сообщения",
|
||||
"translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.",
|
||||
"translation_translateTo": "Перевести на {language}",
|
||||
"translation_translationOptions": "Варианты перевода",
|
||||
"translation_systemLanguage": "Язык системы",
|
||||
"scanner_linuxPairingShowPin": "Показать PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
|
||||
"scanner_linuxPairingHidePin": "Скрыть PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth"
|
||||
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
|
||||
"repeater_cliQuickDiscovery": "Обнаружить Соседей",
|
||||
"repeater_cliQuickClockSync": "Синхронизация часов",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.",
|
||||
"chat_sendMessage": "Отправить сообщение",
|
||||
"repeater_guest": "Информация о ретрансляторе",
|
||||
"room_guest": "Информация о сервере",
|
||||
"repeater_guestTools": "Инструменты для гостей",
|
||||
"common_done": "Готово",
|
||||
"background_serviceTitle": "MeshCore работает",
|
||||
"background_serviceText": "Поддерживает BLE-соединение",
|
||||
"appSettings_translationModelDeleted": "Удалено {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Не удалось удалить: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Не удалось обновить канал: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Тип",
|
||||
"map_path": "Путь",
|
||||
"map_location": "Местоположение",
|
||||
"map_estLocation": "Прибл. местоположение",
|
||||
"map_publicKey": "Публичный ключ",
|
||||
"map_publicKeyPrefixHint": "напр. ab12",
|
||||
"contact_typeChat": "Чат",
|
||||
"contact_typeRepeater": "Ретранслятор",
|
||||
"contact_typeRoom": "Комната",
|
||||
"contact_typeSensor": "Датчик",
|
||||
"contact_typeUnknown": "Неизвестно",
|
||||
"channels_via": "через {path}",
|
||||
"chat_score": "Оценка",
|
||||
"settings_multiAck": "Несколько подтверждений",
|
||||
"map_sharedAt": "Поделено",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Щелкните по заблокированной области, чтобы выделить ее на карте.",
|
||||
"losBlockedSpotsTitle": "Зарезервированные места",
|
||||
"losSelectedObstructionTitle": "Выбранный объект, препятствующий движению",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+143
-11
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Ochranný režim je povolený.",
|
||||
"settings_privacyModeDisabled": "Ochranný režim je vypnutý",
|
||||
"settings_actions": "Možné akcie",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Odoslať reklamu",
|
||||
"settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.",
|
||||
"settings_advertisementSent": "Reklama odeslaná",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_lastSeen": "Naposledy videný",
|
||||
"contact_teleBase": "Báza telemetrie",
|
||||
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
|
||||
"appSettings_initialRouteWeight": "Počiatočná váha trasy",
|
||||
@@ -1941,7 +1936,7 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
|
||||
"settings_multiAck": "Viaceré ACK: {value}",
|
||||
"settings_multiAck": "Viaceré ACK",
|
||||
"map_showOverlaps": "Prekrývanie opakovača kľúča",
|
||||
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2007,6 +2002,34 @@
|
||||
"radioStats_stripWaiting": "Získavanie údajov o rádiu…",
|
||||
"radioStats_settingsTile": "Štatistiky rádiových vysielaní",
|
||||
"radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.",
|
||||
"translation_enableTitle": "Aktivovať preklad",
|
||||
"translation_composerTitle": "Preložte pred odeslaním",
|
||||
"translation_title": "Preklad",
|
||||
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
|
||||
"translation_targetLanguage": "Cieľový jazyk",
|
||||
"translation_useAppLanguage": "Použite jazyk aplikácie",
|
||||
"translation_downloadedModelLabel": "Stiahnutý model",
|
||||
"translation_presetModelLabel": "Prednastavený model od Hugging Face",
|
||||
"translation_manualUrlLabel": "Odkaz na manuál (v elektronickej forme)",
|
||||
"translation_downloadModel": "Stiahnuť model",
|
||||
"translation_downloading": "Stiahnutie...",
|
||||
"translation_working": "Práca...",
|
||||
"translation_stop": "Zastavte",
|
||||
"translation_mergingChunks": "Sliečenie stiahnutých častí do konečného súboru...",
|
||||
"translation_downloadedModels": "Stiahnuté modely",
|
||||
"translation_deleteModel": "Odstrániť model",
|
||||
"translation_modelDownloaded": "Model pre preklad bol stiahnutý.",
|
||||
"translation_downloadStopped": "Stiahnutie bolo prerušené.",
|
||||
"translation_downloadFailed": "Neúspešné stiahnutie: {error}",
|
||||
"translation_enterUrlFirst": "Najprv zadajte URL pre konkrétny model.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2037,117 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak nie je, nechajte prázdne).",
|
||||
"scanner_linuxPairingShowPin": "Zobraziť PIN",
|
||||
"scanner_linuxPairingHidePin": "Skryť PIN",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth párovací PIN"
|
||||
"scanner_linuxPairingShowPin": "Zobraziť PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak neexistuje, nechajte prázdne).",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Posielajte správy v pôvodnej písanom jazyku.",
|
||||
"translation_composerEnabledHint": "Správy budú preložené, než budú odoslané.",
|
||||
"translation_translateBeforeSending": "Preložte pred odeslaním",
|
||||
"translation_messageTranslation": "Preklad textu",
|
||||
"translation_translateTo": "Preložte do {language}",
|
||||
"translation_translationOptions": "Možnosti prekladania",
|
||||
"translation_systemLanguage": "Jazyk systému",
|
||||
"repeater_cliQuickClockSync": "Synchronizácia hodin",
|
||||
"repeater_cliQuickDiscovery": "Objaviť susedov",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Synchronizácia hodiniek po prihlávení",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.",
|
||||
"chat_sendMessage": "Odoslať správu",
|
||||
"repeater_guest": "Informácie o opakovači",
|
||||
"room_guest": "Informácie o serveri",
|
||||
"repeater_guestTools": "Nástroje pre hostí",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "Zdieľané",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "Zablokované miesta",
|
||||
"losBlockedSpotsHint": "Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.",
|
||||
"losSelectedObstructionTitle": "Vybraná prekážka",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+141
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Privatni način je omogočen.",
|
||||
"settings_privacyModeDisabled": "Privatni način je onemogočen.",
|
||||
"settings_actions": "Akcije",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Pošlji Oglas",
|
||||
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
|
||||
"settings_advertisementSent": "Oglas poslan",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_teleEnv": "Okolje telemetrije",
|
||||
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
|
||||
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
|
||||
"appSettings_initialRouteWeight": "Izvirna teža poti",
|
||||
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
|
||||
@@ -1940,7 +1935,6 @@
|
||||
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Večkratni potrditvi: {value}",
|
||||
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
|
||||
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
|
||||
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.",
|
||||
@@ -2007,6 +2001,34 @@
|
||||
"radioStats_stripWaiting": "Prejemanje statistike o radiju…",
|
||||
"radioStats_settingsTile": "Radijske statistike",
|
||||
"radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Preprištejte, preden pošljete",
|
||||
"translation_title": "Prevod",
|
||||
"translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.",
|
||||
"translation_enableTitle": "Omogočite prevod",
|
||||
"translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.",
|
||||
"translation_targetLanguage": "Ciljna jezika",
|
||||
"translation_useAppLanguage": "Uporabite jezik aplikacije",
|
||||
"translation_downloadedModelLabel": "Naložen model",
|
||||
"translation_presetModelLabel": "Prednastavljeni model Hugging Face",
|
||||
"translation_manualUrlLabel": "URL za ročni model",
|
||||
"translation_downloadModel": "Prenesite model",
|
||||
"translation_downloading": "Izvajanje...",
|
||||
"translation_working": "Delo...",
|
||||
"translation_stop": "Prekliji",
|
||||
"translation_mergingChunks": "Sklapljanje prenesenih delov v končni datoteko...",
|
||||
"translation_downloadedModels": "Naloženi modeli",
|
||||
"translation_deleteModel": "Izbrisati model",
|
||||
"translation_modelDownloaded": "Model za prevajanje je bil naložen.",
|
||||
"translation_downloadStopped": "Prenos je bil prekinjen.",
|
||||
"translation_downloadFailed": "Izgovoritev ni bila uspešna: {error}",
|
||||
"translation_enterUrlFirst": "Najprej vnesite URL model.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2036,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "Preprištejte, preden pošljete",
|
||||
"translation_composerDisabledHint": "Pošljite sporočila v originalnem tipkanem jeziku.",
|
||||
"translation_composerEnabledHint": "Vsebina sporočil bo prevedena, preden jih pošljemo.",
|
||||
"translation_messageTranslation": "Prevod sporočila",
|
||||
"translation_translateTo": "Prevesti v {language}",
|
||||
"translation_translationOptions": "Možnosti prevoda",
|
||||
"translation_systemLanguage": "Jezik sistema",
|
||||
"scanner_linuxPairingShowPin": "Prikaži PIN",
|
||||
"scanner_linuxPairingHidePin": "Skrij PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje"
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
|
||||
"repeater_cliQuickDiscovery": "Odkrijte sosede",
|
||||
"repeater_cliQuickClockSync": "Usklajevanje ure",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.",
|
||||
"repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi",
|
||||
"repeater_guest": "Informacije o ponovljalniku",
|
||||
"chat_sendMessage": "Pošlji sporočilo",
|
||||
"room_guest": "Informacije o strežniku",
|
||||
"repeater_guestTools": "Naložila za goste",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Več potrdil",
|
||||
"map_sharedAt": "Deljeno",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Dotaknite blokirano točko, da jo označite na zemljeplati.",
|
||||
"losSelectedObstructionTitle": "Izbrano ovire",
|
||||
"losBlockedSpotsTitle": "Zasedena parkirišča",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+141
-9
@@ -104,6 +104,8 @@
|
||||
"settings_privacyModeEnabled": "Privatläget är aktiverat",
|
||||
"settings_privacyModeDisabled": "Privatläge är avstängt",
|
||||
"settings_actions": "Åtgärder",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "Skicka Annons",
|
||||
"settings_sendAdvertisementSubtitle": "Sändning finns nu",
|
||||
"settings_advertisementSent": "Annons skickad",
|
||||
@@ -1922,13 +1924,6 @@
|
||||
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
|
||||
"contact_teleLoc": "Telemetridata plats",
|
||||
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
|
||||
"appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
|
||||
@@ -1941,7 +1936,6 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Repeater-nyckelöverlappningar",
|
||||
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg",
|
||||
"@radioStats_noiseFloor": {
|
||||
@@ -2007,6 +2001,34 @@
|
||||
"radioStats_stripWaiting": "Hämtar radiostatistik…",
|
||||
"radioStats_settingsTile": "Radiostation",
|
||||
"radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Översätt inkommande meddelanden och möjliggör översättning före avsändning.",
|
||||
"translation_enableTitle": "Aktivera översättning",
|
||||
"translation_title": "Översättning",
|
||||
"translation_composerTitle": "Översätt innan du skickar",
|
||||
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
|
||||
"translation_targetLanguage": "Målmedvetet språk",
|
||||
"translation_useAppLanguage": "Använd appens språk",
|
||||
"translation_downloadedModelLabel": "Nedladdad modell",
|
||||
"translation_presetModelLabel": "Fördefinierat Hugging Face-modell",
|
||||
"translation_manualUrlLabel": "Manualens URL",
|
||||
"translation_downloadModel": "Ladda ner modellen",
|
||||
"translation_downloading": "Nedladdning...",
|
||||
"translation_working": "Arbeta...",
|
||||
"translation_stop": "Stopp",
|
||||
"translation_mergingChunks": "Slå samman de nedladdade delarna till en slutlig fil...",
|
||||
"translation_downloadedModels": "Nedladdade modeller",
|
||||
"translation_deleteModel": "Ta bort modell",
|
||||
"translation_modelDownloaded": "Översättningsmodellen har laddats ner.",
|
||||
"translation_downloadStopped": "Nedladdningen avbruten.",
|
||||
"translation_downloadFailed": "Nedladdning misslyckades: {error}",
|
||||
"translation_enterUrlFirst": "Ange först en URL för en specifik modell.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2036,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Skicka meddelanden på det ursprungliga, stavade språket.",
|
||||
"translation_translateBeforeSending": "Översätt innan du skickar",
|
||||
"translation_composerEnabledHint": "Meddelandena kommer att översättas innan de skickas.",
|
||||
"translation_messageTranslation": "Meddelandets översättning",
|
||||
"translation_translateTo": "Översätt till {language}",
|
||||
"translation_translationOptions": "Översättningsalternativ",
|
||||
"translation_systemLanguage": "Språk för systemet",
|
||||
"scanner_linuxPairingShowPin": "Visa PIN",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
|
||||
"scanner_linuxPairingHidePin": "Dölj PIN"
|
||||
"scanner_linuxPairingHidePin": "Dölj PIN",
|
||||
"repeater_cliQuickDiscovery": "Upptäck grannar",
|
||||
"repeater_cliQuickClockSync": "Synkronisera klocka",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.",
|
||||
"repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning",
|
||||
"repeater_guest": "Information om repetorer",
|
||||
"chat_sendMessage": "Skicka meddelande",
|
||||
"repeater_guestTools": "Gästverktyg",
|
||||
"room_guest": "Information om servern",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"settings_multiAck": "Flera bekräftelser",
|
||||
"map_sharedAt": "Delad",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losBlockedSpotsTitle": "Reserverade platser",
|
||||
"losSelectedObstructionTitle": "Vald hinder",
|
||||
"losBlockedSpotsHint": "Klicka på en markerad plats för att framhäva den på kartan.",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+291
-180
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
|
||||
"channels_channelDeleteFailed": "Не вдалось видалити канал \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -18,6 +18,7 @@
|
||||
"common_save": "Зберегти",
|
||||
"common_delete": "Видалити",
|
||||
"common_close": "Закрити",
|
||||
"common_done": "Готово",
|
||||
"common_edit": "Редагувати",
|
||||
"common_add": "Додати",
|
||||
"common_settings": "Налаштування",
|
||||
@@ -26,7 +27,7 @@
|
||||
"common_disconnected": "Відключено",
|
||||
"common_create": "Створити",
|
||||
"common_continue": "Продовжити",
|
||||
"common_share": "Поділитися",
|
||||
"common_share": "Поділитись",
|
||||
"common_copy": "Копіювати",
|
||||
"common_retry": "Повторити",
|
||||
"common_hide": "Приховати",
|
||||
@@ -81,7 +82,7 @@
|
||||
"device_meshcore": "MeshCore",
|
||||
"settings_title": "Налаштування",
|
||||
"settings_deviceInfo": "Інформація про пристрій",
|
||||
"settings_appSettings": "Налаштування програми",
|
||||
"settings_appSettings": "Налаштування застосунку",
|
||||
"settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти",
|
||||
"settings_nodeSettings": "Налаштування вузла",
|
||||
"settings_nodeName": "Ім'я вузла",
|
||||
@@ -91,19 +92,21 @@
|
||||
"settings_radioSettings": "Налаштування радіо",
|
||||
"settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення",
|
||||
"settings_radioSettingsUpdated": "Налаштування радіо оновлено",
|
||||
"settings_location": "Розташування",
|
||||
"settings_locationSubtitle": "GPS координати",
|
||||
"settings_locationUpdated": "Розташування оновлено",
|
||||
"settings_location": "Геопозиція",
|
||||
"settings_locationSubtitle": "GPS-координати",
|
||||
"settings_locationUpdated": "Геопозицію оновлено",
|
||||
"settings_locationBothRequired": "Введіть широту та довготу.",
|
||||
"settings_locationInvalid": "Некоректна широта або довгота.",
|
||||
"settings_latitude": "Широта",
|
||||
"settings_longitude": "Довгота",
|
||||
"settings_privacyMode": "Режим приватності",
|
||||
"settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях",
|
||||
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.",
|
||||
"settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
|
||||
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
|
||||
"settings_privacyModeEnabled": "Режим приватності увімкнено",
|
||||
"settings_privacyModeDisabled": "Режим приватності вимкнено",
|
||||
"settings_actions": "Дії",
|
||||
"settings_deleteAllPaths": "Видалити всі шляхи",
|
||||
"settings_deleteAllPathsSubtitle": "Очистити всі дані шляхів у контактах.",
|
||||
"settings_sendAdvertisement": "Оголосити себе",
|
||||
"settings_sendAdvertisementSubtitle": "Транслювати присутність зараз",
|
||||
"settings_advertisementSent": "Оголошення надіслано",
|
||||
@@ -118,9 +121,9 @@
|
||||
"settings_debug": "Налагодження",
|
||||
"settings_bleDebugLog": "Журнал налагодження BLE",
|
||||
"settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані",
|
||||
"settings_appDebugLog": "Журнал налагодження програми",
|
||||
"settings_appDebugLogSubtitle": "Повідомлення налагодження програми",
|
||||
"settings_about": "Про програму",
|
||||
"settings_appDebugLog": "Журнал налагодження застосунку",
|
||||
"settings_appDebugLogSubtitle": "Повідомлення налагодження застосунку",
|
||||
"settings_about": "Про застосунок",
|
||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||
"@settings_aboutVersion": {
|
||||
"placeholders": {
|
||||
@@ -156,7 +159,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_title": "Налаштування програми",
|
||||
"appSettings_title": "Налаштування застосунку",
|
||||
"appSettings_appearance": "Вигляд",
|
||||
"appSettings_theme": "Тема",
|
||||
"appSettings_themeSystem": "Системна",
|
||||
@@ -194,9 +197,9 @@
|
||||
"appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб",
|
||||
"appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання",
|
||||
"appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.",
|
||||
"appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.",
|
||||
"appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатись автоматично.",
|
||||
"appSettings_autoRouteRotation": "Авторотація маршруту",
|
||||
"appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)",
|
||||
"appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «через всю мережу» (flood)",
|
||||
"appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена",
|
||||
"appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена",
|
||||
"appSettings_battery": "Батарея",
|
||||
@@ -251,10 +254,10 @@
|
||||
}
|
||||
},
|
||||
"appSettings_debugCard": "Налагодження",
|
||||
"appSettings_appDebugLogging": "Логування налагодження програми",
|
||||
"appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.",
|
||||
"appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено",
|
||||
"appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.",
|
||||
"appSettings_appDebugLogging": "Журналювання налагодження застосунку",
|
||||
"appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження застосунку в журнал для усунення несправностей.",
|
||||
"appSettings_appDebugLoggingEnabled": "Журналювання налагодження застосунку увімкнено",
|
||||
"appSettings_appDebugLoggingDisabled": "Журналювання налагодження застосунку вимкнено.",
|
||||
"contacts_title": "Контакти",
|
||||
"contacts_noContacts": "Контактів не знайдено.",
|
||||
"contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.",
|
||||
@@ -338,8 +341,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "Канал з хештегом",
|
||||
"channels_hashtagChannel": "Хештег-канал",
|
||||
"channels_public": "Публічний",
|
||||
"channels_via": "через {path}",
|
||||
"channels_private": "Приватний",
|
||||
"channels_publicChannel": "Публічний канал",
|
||||
"channels_privateChannel": "Приватний канал",
|
||||
@@ -371,7 +375,7 @@
|
||||
"channels_pskHex": "PSK (Hex)",
|
||||
"channels_generateRandomPsk": "Згенерувати випадковий ключ PSK",
|
||||
"channels_enterChannelName": "Будь ласка, введіть назву каналу",
|
||||
"channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.",
|
||||
"channels_pskMustBe32Hex": "PSK має складатись з 32 шістнадцяткових символів.",
|
||||
"channels_channelAdded": "Канал «{name}» додано",
|
||||
"@channels_channelAdded": {
|
||||
"placeholders": {
|
||||
@@ -422,7 +426,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_location": "Розташування",
|
||||
"chat_location": "Геопозиція",
|
||||
"chat_sendMessageTo": "Надіслати повідомлення {contactName}",
|
||||
"@chat_sendMessageTo": {
|
||||
"placeholders": {
|
||||
@@ -466,17 +470,17 @@
|
||||
"gifPicker_searchHint": "Пошук GIF...",
|
||||
"gifPicker_poweredBy": "На базі GIPHY",
|
||||
"gifPicker_noGifsFound": "GIF не знайдено",
|
||||
"gifPicker_failedLoad": "Не вдалося завантажити GIF-файли",
|
||||
"gifPicker_failedLoad": "Не вдалось завантажити GIF-файли",
|
||||
"gifPicker_failedSearch": "Пошук GIF не вдався",
|
||||
"gifPicker_noInternet": "Немає інтернет-з'єднання",
|
||||
"debugLog_appTitle": "Журнал налагодження програми",
|
||||
"debugLog_appTitle": "Журнал налагодження застосунку",
|
||||
"debugLog_bleTitle": "Журнал налагодження BLE",
|
||||
"debugLog_copyLog": "Копіювати журнал",
|
||||
"debugLog_clearLog": "Очистити журнал",
|
||||
"debugLog_copied": "Журнал налагодження скопійовано",
|
||||
"debugLog_bleCopied": "Журнал BLE скопійовано",
|
||||
"debugLog_noEntries": "Поки що немає записів журналу налагодження.",
|
||||
"debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях",
|
||||
"debugLog_enableInSettings": "Увімкніть налагодження застосунку в налаштуваннях",
|
||||
"debugLog_frames": "Кадри",
|
||||
"debugLog_rawLogRx": "Необроблений лог - RX",
|
||||
"debugLog_noBleActivity": "Поки що немає активності BLE.",
|
||||
@@ -546,12 +550,12 @@
|
||||
"chat_pathManagement": "Керування шляхами",
|
||||
"chat_routingMode": "Режим маршрутизації",
|
||||
"chat_autoUseSavedPath": "Авто (використовувати збережений шлях)",
|
||||
"chat_forceFloodMode": "Примусово на всю мережу",
|
||||
"chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):",
|
||||
"chat_forceFloodMode": "Примусово через всю мережу",
|
||||
"chat_recentAckPaths": "Підтверджені шляхи (натисніть, щоб використати):",
|
||||
"chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.",
|
||||
"chat_hopSingular": "Стрибок",
|
||||
"chat_hopPlural": "стрибків",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}",
|
||||
"chat_hopSingular": "Перехід",
|
||||
"chat_hopPlural": "переходів",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{перехід} few{переходи} many{переходів} other{переходів}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -560,6 +564,7 @@
|
||||
}
|
||||
},
|
||||
"chat_successes": "Успішно",
|
||||
"chat_score": "Оцінка",
|
||||
"chat_removePath": "Видалити шлях",
|
||||
"chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.",
|
||||
"chat_pathActions": "Дії зі шляхом:",
|
||||
@@ -568,11 +573,11 @@
|
||||
"chat_clearPath": "Очистити шлях",
|
||||
"chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні",
|
||||
"chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.",
|
||||
"chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми",
|
||||
"chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.",
|
||||
"chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі застосунку",
|
||||
"chat_floodModeEnabled": "Увімкнено режим «через всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.",
|
||||
"chat_fullPath": "Повний шлях",
|
||||
"chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.",
|
||||
"chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}",
|
||||
"chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{перехід} few{переходи} many{переходів} other{переходів}} - {status}",
|
||||
"@chat_pathSetHops": {
|
||||
"placeholders": {
|
||||
"hopCount": {
|
||||
@@ -590,9 +595,9 @@
|
||||
"chat_path": "Шлях",
|
||||
"chat_publicKey": "Відкритий ключ",
|
||||
"chat_compressOutgoingMessages": "Стискати вихідні повідомлення",
|
||||
"chat_floodForced": "На всю мережу (примусово)",
|
||||
"chat_directForced": "Прямий (примусово)",
|
||||
"chat_hopsForced": "{count} стрибків (примусово)",
|
||||
"chat_floodForced": "Через всю мережу (примусово)",
|
||||
"chat_directForced": "Напряму (примусово)",
|
||||
"chat_hopsForced": "{count} переходів (примусово)",
|
||||
"@chat_hopsForced": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -600,9 +605,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_floodAuto": "На всю мережу (авто)",
|
||||
"chat_direct": "Прямий",
|
||||
"chat_poiShared": "Точкою інтересу поділилися",
|
||||
"chat_floodAuto": "Через всю мережу (авто)",
|
||||
"chat_direct": "Напряму",
|
||||
"chat_poiShared": "Поділилися точкою інтересу",
|
||||
"chat_unread": "Непрочитано: {count}",
|
||||
"@chat_unread": {
|
||||
"placeholders": {
|
||||
@@ -614,7 +619,7 @@
|
||||
"chat_openLink": "Відкрити посилання?",
|
||||
"chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?",
|
||||
"chat_open": "Відкрити",
|
||||
"chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}",
|
||||
"chat_couldNotOpenLink": "Не вдалось відкрити посилання: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
@@ -624,8 +629,8 @@
|
||||
},
|
||||
"chat_invalidLink": "Невірний формат посилання",
|
||||
"map_title": "Карта вузлів",
|
||||
"map_noNodesWithLocation": "Немає вузлів з даними про розташування",
|
||||
"map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.",
|
||||
"map_noNodesWithLocation": "Немає вузлів з даними про геопозицію",
|
||||
"map_nodesNeedGps": "Вузли мають надавати свої GPS координати,\nщоб з'явитись на карті.",
|
||||
"map_nodesCount": "Вузли: {count}",
|
||||
"@map_nodesCount": {
|
||||
"placeholders": {
|
||||
@@ -650,19 +655,25 @@
|
||||
"map_pinPrivate": "Замок (Приватний)",
|
||||
"map_pinPublic": "Ключ (Публічний)",
|
||||
"map_lastSeen": "Останній раз бачили",
|
||||
"map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?",
|
||||
"map_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?",
|
||||
"map_from": "Від",
|
||||
"map_source": "Джерело",
|
||||
"map_flags": "Прапорці",
|
||||
"map_shareMarkerHere": "Поділитися маркером тут",
|
||||
"map_type": "Тип",
|
||||
"map_path": "Шлях",
|
||||
"map_location": "Геопозиція",
|
||||
"map_estLocation": "Орієнтовна геопозиція",
|
||||
"map_publicKey": "Публічний ключ",
|
||||
"map_publicKeyPrefixHint": "напр. ab12",
|
||||
"map_shareMarkerHere": "Поділитись маркером тут",
|
||||
"map_pinLabel": "Мітка піна",
|
||||
"map_label": "Мітка",
|
||||
"map_pointOfInterest": "Точка інтересу",
|
||||
"map_sendToContact": "Надіслати контакту",
|
||||
"map_sendToChannel": "Надіслати в канал",
|
||||
"map_noChannelsAvailable": "Немає доступних каналів",
|
||||
"map_publicLocationShare": "Поділитися в публічному місці",
|
||||
"map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.",
|
||||
"map_publicLocationShare": "Поділитись в публічному місці",
|
||||
"map_publicLocationShareConfirm": "Ви збираєтесь поділитись геопозицією у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.",
|
||||
"@map_publicLocationShareConfirm": {
|
||||
"placeholders": {
|
||||
"channelLabel": {
|
||||
@@ -670,7 +681,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами",
|
||||
"map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитись маркерами",
|
||||
"map_filterNodes": "Фільтрувати вузли",
|
||||
"map_nodeTypes": "Типи вузлів",
|
||||
"map_chatNodes": "Вузли чату",
|
||||
@@ -683,7 +694,7 @@
|
||||
"map_showSharedMarkers": "Показувати спільні маркери",
|
||||
"map_lastSeenTime": "Час останньої активності",
|
||||
"map_sharedPin": "Спільний пін",
|
||||
"map_joinRoom": "Приєднатися до кімнати",
|
||||
"map_joinRoom": "Приєднатись до кімнати",
|
||||
"map_manageRepeater": "Керувати ретранслятором",
|
||||
"mapCache_title": "Офлайн-кеш карти",
|
||||
"mapCache_selectAreaFirst": "Спершу виберіть область для кешування",
|
||||
@@ -806,7 +817,7 @@
|
||||
"time_minutes": "хвилин",
|
||||
"time_allTime": "Весь час",
|
||||
"dialog_disconnect": "Відключити",
|
||||
"dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?",
|
||||
"dialog_disconnectConfirm": "Ви впевнені, що хочете відключитись від цього пристрою?",
|
||||
"login_repeaterLogin": "Вхід у ретранслятор",
|
||||
"login_roomLogin": "Вхід у кімнату",
|
||||
"login_password": "Пароль",
|
||||
@@ -817,8 +828,8 @@
|
||||
"login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.",
|
||||
"login_routing": "Маршрутизація",
|
||||
"login_routingMode": "Режим маршрутизації",
|
||||
"login_autoUseSavedPath": "Авто (використовувати збережений шлях)",
|
||||
"login_forceFloodMode": "Примусово на всю мережу",
|
||||
"login_autoUseSavedPath": "Авто (збережений шлях)",
|
||||
"login_forceFloodMode": "Примусово через всю мережу",
|
||||
"login_managePaths": "Керувати шляхами",
|
||||
"login_login": "Вхід",
|
||||
"login_attempt": "Спроба {current}/{max}",
|
||||
@@ -851,7 +862,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}",
|
||||
"path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{переходом} few{переходами} many{переходами} other{переходами}}",
|
||||
"@path_usingHopsPath": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -861,10 +872,10 @@
|
||||
},
|
||||
"path_enterCustomPath": "Ввести власний шлях",
|
||||
"path_currentPathLabel": "Поточний шлях",
|
||||
"path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.",
|
||||
"path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного переходу, розділені комами.",
|
||||
"path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).",
|
||||
"path_labelHexPrefixes": "Hex-префікси",
|
||||
"path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)",
|
||||
"path_helperMaxHops": "Макс. 64 переходи. Кожен префікс — 2 шістнадцяткові символи (1 байт)",
|
||||
"path_selectFromContacts": "Вибрати з контактів:",
|
||||
"path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.",
|
||||
"path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.",
|
||||
@@ -876,7 +887,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.",
|
||||
"path_tooLong": "Шлях занадто довгий. Максимум 64 переходи.",
|
||||
"path_setPath": "Встановити шлях",
|
||||
"repeater_management": "Керування ретранслятором",
|
||||
"repeater_managementTools": "Інструменти керування",
|
||||
@@ -891,7 +902,7 @@
|
||||
"repeater_statusTitle": "Статус ретранслятора",
|
||||
"repeater_routingMode": "Режим маршрутизації",
|
||||
"repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)",
|
||||
"repeater_forceFloodMode": "Примусово на всю мережу",
|
||||
"repeater_forceFloodMode": "Примусово через всю мережу",
|
||||
"repeater_pathManagement": "Керування шляхами",
|
||||
"repeater_refresh": "Оновити",
|
||||
"repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.",
|
||||
@@ -936,7 +947,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}",
|
||||
"repeater_packetTxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}",
|
||||
"@repeater_packetTxTotal": {
|
||||
"placeholders": {
|
||||
"total": {
|
||||
@@ -950,7 +961,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}",
|
||||
"repeater_packetRxTotal": "Всього: {total}, Через всю мережу: {flood}, Прямі: {direct}",
|
||||
"@repeater_packetRxTotal": {
|
||||
"placeholders": {
|
||||
"total": {
|
||||
@@ -964,7 +975,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}",
|
||||
"repeater_duplicatesFloodDirect": "Через всю мережу: {flood}, Прямі: {direct}",
|
||||
"@repeater_duplicatesFloodDirect": {
|
||||
"placeholders": {
|
||||
"flood": {
|
||||
@@ -999,7 +1010,7 @@
|
||||
"repeater_bandwidth": "Смуга пропускання",
|
||||
"repeater_spreadingFactor": "Коефіцієнт розширення",
|
||||
"repeater_codingRate": "Швидкість кодування",
|
||||
"repeater_locationSettings": "Налаштування розташування",
|
||||
"repeater_locationSettings": "Налаштування геопозиції",
|
||||
"repeater_latitude": "Широта",
|
||||
"repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)",
|
||||
"repeater_longitude": "Довгота",
|
||||
@@ -1010,9 +1021,9 @@
|
||||
"repeater_guestAccess": "Гостьовий доступ",
|
||||
"repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання",
|
||||
"repeater_privacyMode": "Режим приватності",
|
||||
"repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях",
|
||||
"repeater_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
|
||||
"repeater_advertisementSettings": "Налаштування оголошень",
|
||||
"repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)",
|
||||
"repeater_localAdvertInterval": "Інтервал локальних оголошень (без ретрансляції)",
|
||||
"repeater_localAdvertIntervalMinutes": "{minutes} хвилин",
|
||||
"@repeater_localAdvertIntervalMinutes": {
|
||||
"placeholders": {
|
||||
@@ -1021,7 +1032,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)",
|
||||
"repeater_floodAdvertInterval": "Інтервал оголошень через всю мережу (flood)",
|
||||
"repeater_floodAdvertIntervalHours": "{hours} годин",
|
||||
"@repeater_floodAdvertIntervalHours": {
|
||||
"placeholders": {
|
||||
@@ -1035,9 +1046,9 @@
|
||||
"repeater_rebootRepeater": "Перезавантажити ретранслятор",
|
||||
"repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора",
|
||||
"repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?",
|
||||
"repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності",
|
||||
"repeater_regenerateIdentityKey": "Перегенерувати ключ ідентифікації",
|
||||
"repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентифікацію для ретранслятора. Продовжити?",
|
||||
"repeater_eraseFileSystem": "Очистити файлову систему",
|
||||
"repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора",
|
||||
"repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!",
|
||||
@@ -1071,7 +1082,7 @@
|
||||
"repeater_refreshBasicSettings": "Оновити основні налаштування",
|
||||
"repeater_refreshRadioSettings": "Оновити налаштування радіо",
|
||||
"repeater_refreshTxPower": "Оновити потужність TX",
|
||||
"repeater_refreshLocationSettings": "Оновити налаштування розташування",
|
||||
"repeater_refreshLocationSettings": "Оновити налаштування геопозиції",
|
||||
"repeater_refreshPacketForwarding": "Оновити пересилання пакетів",
|
||||
"repeater_refreshGuestAccess": "Оновити гостьовий доступ",
|
||||
"repeater_refreshPrivacyMode": "Оновити режим приватності",
|
||||
@@ -1128,19 +1139,19 @@
|
||||
"repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).",
|
||||
"repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.",
|
||||
"repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)",
|
||||
"repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).",
|
||||
"repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість переходів для вхідних пакетів flood (якщо >= max, пакет не пересилається).",
|
||||
"repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.",
|
||||
"repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.",
|
||||
"repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення через всю мережу. Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)",
|
||||
"repeater_cliHelpSetName": "Встановлює ім'я для оголошення.",
|
||||
"repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)",
|
||||
"repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)",
|
||||
"repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.",
|
||||
"repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.",
|
||||
"repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).",
|
||||
"repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «через всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).",
|
||||
"repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.",
|
||||
"repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.",
|
||||
"repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.",
|
||||
@@ -1156,7 +1167,7 @@
|
||||
"repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.",
|
||||
"repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4",
|
||||
"repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.",
|
||||
"repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).",
|
||||
"repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «через всю мережу» (flood).",
|
||||
"repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.",
|
||||
"repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»",
|
||||
"repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.",
|
||||
@@ -1170,14 +1181,14 @@
|
||||
"repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.",
|
||||
"repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.",
|
||||
"repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.",
|
||||
"repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях",
|
||||
"repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.",
|
||||
"repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення геопозиції вузла:\n- none : не включати геопозицію в оголошення\n- share : ділитись геопозицією GPS (з SensorManager)\n- prefs : оголошувати геопозицію, збережену в налаштуваннях",
|
||||
"repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення геопозиції.",
|
||||
"repeater_commandsListTitle": "Список команд",
|
||||
"repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...",
|
||||
"repeater_general": "Загальні",
|
||||
"repeater_settingsCategory": "Налаштування",
|
||||
"repeater_bridge": "Міст",
|
||||
"repeater_logging": "Логування",
|
||||
"repeater_logging": "Журналювання",
|
||||
"repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)",
|
||||
"repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)",
|
||||
"repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.",
|
||||
@@ -1248,7 +1259,7 @@
|
||||
"channelPath_title": "Шлях пакету",
|
||||
"channelPath_viewMap": "Показати карту",
|
||||
"channelPath_otherObservedPaths": "Інші спостережувані шляхи",
|
||||
"channelPath_repeaterHops": "Стрибки ретранслятора",
|
||||
"channelPath_repeaterHops": "Переходи через ретранслятори",
|
||||
"channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.",
|
||||
"channelPath_messageDetails": "Деталі повідомлення",
|
||||
"channelPath_senderLabel": "Відправник",
|
||||
@@ -1267,7 +1278,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channelPath_noLocationData": "Немає даних про розташування",
|
||||
"channelPath_noLocationData": "Немає даних про геопозицію",
|
||||
"channelPath_timeWithDate": "{day}/{month} {time}",
|
||||
"@channelPath_timeWithDate": {
|
||||
"placeholders": {
|
||||
@@ -1291,9 +1302,9 @@
|
||||
}
|
||||
},
|
||||
"channelPath_unknownPath": "Невідомий",
|
||||
"channelPath_floodPath": "На всю мережу",
|
||||
"channelPath_directPath": "Прямий",
|
||||
"channelPath_observedZeroOf": "0 з {total} стрибків",
|
||||
"channelPath_floodPath": "Через всю мережу",
|
||||
"channelPath_directPath": "Напряму",
|
||||
"channelPath_observedZeroOf": "0 з {total} переходів",
|
||||
"@channelPath_observedZeroOf": {
|
||||
"placeholders": {
|
||||
"total": {
|
||||
@@ -1301,7 +1312,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channelPath_observedSomeOf": "{observed} з {total} стрибків",
|
||||
"channelPath_observedSomeOf": "{observed} з {total} переходів",
|
||||
"@channelPath_observedSomeOf": {
|
||||
"placeholders": {
|
||||
"observed": {
|
||||
@@ -1342,12 +1353,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.",
|
||||
"channelPath_noHopDetailsAvailable": "Деталі переходів недоступні для цього пакету.",
|
||||
"channelPath_unknownRepeater": "Невідомий ретранслятор",
|
||||
"listFilter_tooltip": "Фільтр та сортування",
|
||||
"listFilter_sortBy": "Сортувати за",
|
||||
"listFilter_latestMessages": "Останні повідомлення",
|
||||
"listFilter_heardRecently": "Нещодавно чули",
|
||||
"listFilter_heardRecently": "Нещодавно почуті",
|
||||
"listFilter_az": "А-Я",
|
||||
"listFilter_filters": "Фільтри",
|
||||
"listFilter_all": "Все",
|
||||
@@ -1364,20 +1375,20 @@
|
||||
}
|
||||
},
|
||||
"repeater_neighbors": "Сусіди",
|
||||
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
|
||||
"repeater_neighborsSubtitle": "Показати сусідів, доступних без ретрансляції.",
|
||||
"neighbors_receivedData": "Дані сусідів отримано",
|
||||
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
||||
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
||||
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
|
||||
"neighbors_noData": "Дані про сусідів недоступні.",
|
||||
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
||||
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
||||
"channels_joinPrivateChannel": "Приєднатись до приватного каналу",
|
||||
"channels_createPrivateChannel": "Створити приватний канал",
|
||||
"channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.",
|
||||
"channels_joinPublicChannel": "Приєднатися до публічного каналу",
|
||||
"channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.",
|
||||
"channels_joinHashtagChannel": "Приєднатися до каналу з хештегом",
|
||||
"channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.",
|
||||
"channels_joinPublicChannel": "Приєднатись до публічного каналу",
|
||||
"channels_joinPublicChannelDesc": "Будь-хто може приєднатись до цього каналу.",
|
||||
"channels_joinHashtagChannel": "Приєднатись до хештег-каналу",
|
||||
"channels_joinHashtagChannelDesc": "Будь-хто може приєднатись до хештег-каналів.",
|
||||
"channels_scanQrCode": "Сканувати QR-код",
|
||||
"channels_scanQrCodeComingSoon": "Скоро буде",
|
||||
"channels_enterHashtag": "Введіть хештег",
|
||||
@@ -1399,7 +1410,7 @@
|
||||
"neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}",
|
||||
"neighbors_heardAgo": "Почуто: {time} тому",
|
||||
"settings_locationGPSEnable": "Увімкнути GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.",
|
||||
"settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення геопозиції через GPS.",
|
||||
"settings_locationIntervalSec": "Інтервал для GPS (Секунди)",
|
||||
"settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.",
|
||||
"contacts_manageRoom": "Керувати сервером кімнати",
|
||||
@@ -1463,10 +1474,10 @@
|
||||
"common_ok": "ОК",
|
||||
"community_title": "Спільнота",
|
||||
"community_create": "Створити спільноту",
|
||||
"community_createDesc": "Створити нову спільноту та поділитися через QR-код.",
|
||||
"community_join": "Приєднатися",
|
||||
"community_joinTitle": "Приєднатися до спільноти",
|
||||
"community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?",
|
||||
"community_createDesc": "Створити нову спільноту та поділитись через QR-код.",
|
||||
"community_join": "Приєднатись",
|
||||
"community_joinTitle": "Приєднатись до спільноти",
|
||||
"community_joinConfirmation": "Ви бажаєте приєднатись до спільноти «{name}»?",
|
||||
"community_scanQr": "Сканувати QR спільноти",
|
||||
"community_scanInstructions": "Наведіть камеру на QR-код спільноти.",
|
||||
"community_showQr": "Показати QR-код",
|
||||
@@ -1476,9 +1487,9 @@
|
||||
"community_enterName": "Введіть назву спільноти",
|
||||
"community_created": "Спільноту «{name}» створено",
|
||||
"community_joined": "Приєднався до спільноти «{name}»",
|
||||
"community_qrTitle": "Поділитися спільнотою",
|
||||
"community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}",
|
||||
"community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти",
|
||||
"community_qrTitle": "Поділитись спільнотою",
|
||||
"community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатись до {name}",
|
||||
"community_hashtagPrivacyHint": "Хештег-канали спільноти доступні лише членам спільноти",
|
||||
"community_invalidQrCode": "Недійсний QR-код спільноти",
|
||||
"community_alreadyMember": "Вже учасник",
|
||||
"community_alreadyMemberMessage": "Ви вже є учасником «{name}».",
|
||||
@@ -1499,10 +1510,10 @@
|
||||
},
|
||||
"community_deleted": "Спільноту «{name}» покинуто",
|
||||
"community_addHashtagChannel": "Додати хештег спільноти",
|
||||
"community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти",
|
||||
"community_addHashtagChannelDesc": "Додати хештег-канал для цієї спільноти",
|
||||
"community_selectCommunity": "Вибрати спільноту",
|
||||
"community_regularHashtag": "Звичайний хештег",
|
||||
"community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)",
|
||||
"community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатись)",
|
||||
"community_communityHashtag": "Хештег спільноти",
|
||||
"community_communityHashtagDesc": "Ексклюзивно для членів спільноти",
|
||||
"community_forCommunity": "Для {name}",
|
||||
@@ -1534,13 +1545,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerateSecret": "Перегенерувати секрет",
|
||||
"community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.",
|
||||
"community_regenerateSecret": "Перегенерувати секретний ключ",
|
||||
"community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Усі учасники матимуть відсканувати новий QR-код, щоб продовжити спілкування.",
|
||||
"community_regenerate": "Перегенерувати",
|
||||
"community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано",
|
||||
"community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»",
|
||||
"community_updateSecret": "Оновити секрет",
|
||||
"community_secretUpdated": "Зміну секрету для «{name}» оновлено",
|
||||
"community_secretRegenerated": "Секретний ключ для «{name}» перегенеровано",
|
||||
"community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити секретний ключ для «{name}»",
|
||||
"community_updateSecret": "Оновити секретний ключ",
|
||||
"community_secretUpdated": "Секретний ключ для «{name}» оновлено",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -1549,76 +1560,76 @@
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Ви",
|
||||
"pathTrace_failed": "Відстеження шляху не вдалося.",
|
||||
"pathTrace_failed": "Відстеження шляху не вдалось.",
|
||||
"pathTrace_notAvailable": "Трасування шляху недоступне.",
|
||||
"pathTrace_refreshTooltip": "Оновити Path Trace",
|
||||
"pathTrace_refreshTooltip": "Оновити трасування шляху",
|
||||
"contacts_pathTrace": "Трасування шляхів",
|
||||
"contacts_ping": "Пінгувати",
|
||||
"contacts_repeaterPathTrace": "Трасування шляху до повторювача",
|
||||
"contacts_repeaterPing": "Пінгувати повторювач",
|
||||
"contacts_repeaterPathTrace": "Трасування шляху до ретранслятора",
|
||||
"contacts_repeaterPing": "Пінгувати ретранслятор",
|
||||
"contacts_roomPathTrace": "Трасування шляху до серверу кімнати",
|
||||
"contacts_roomPing": "Пінг сервера кімнати",
|
||||
"contacts_chatTraceRoute": "Трасування шляху",
|
||||
"contacts_pathTraceTo": "Відстежити маршрут до {name}",
|
||||
"contacts_invalidAdvertFormat": "Недійсні контактні дані",
|
||||
"contacts_contactImported": "Контакт було імпортовано.",
|
||||
"contacts_contactImportFailed": "Контакт не вдалося імпортувати",
|
||||
"contacts_zeroHopAdvert": "Реклама без перехоплення",
|
||||
"contacts_floodAdvert": "Залив реклами",
|
||||
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
||||
"contacts_contactImportFailed": "Контакт не вдалось імпортувати",
|
||||
"contacts_zeroHopAdvert": "Оголошення без ретрансляції",
|
||||
"contacts_floodAdvert": "Оголошення з ретрансляцією",
|
||||
"contacts_copyAdvertToClipboard": "Копіювати оголошення",
|
||||
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
||||
"appSettings_languageRu": "Російська",
|
||||
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
|
||||
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
|
||||
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
||||
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
||||
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
||||
"contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало",
|
||||
"contacts_zeroHopContactAdvertFailed": "Не вдалось надіслати контакт.",
|
||||
"contacts_contactAdvertCopied": "Оголошення скопійовано до буфера обміну.",
|
||||
"contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилось невдало",
|
||||
"contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням",
|
||||
"contacts_addContactFromClipboard": "Додати контакт з буфера обміну",
|
||||
"contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням",
|
||||
"contacts_addContactFromClipboard": "Додати контакт з буфера",
|
||||
"contacts_ShareContactZeroHop": "Поділитись контактом за оголошенням",
|
||||
"notification_activityTitle": "Активність MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нові вузли} many{нових вузлів} other{нових вузлів}}",
|
||||
"notification_newTypeDiscovered": "Виявлено новий {contactType}",
|
||||
"notification_receivedNewMessage": "Отримано нове повідомлення",
|
||||
"settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.",
|
||||
"settings_gpxExportRepeaters": "Експорт ретрансляторів і серверів кімнат у GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори та сервери кімнат з геопозицією у файл GPX.",
|
||||
"settings_gpxExportSuccess": "Успішно експортовано файл GPX.",
|
||||
"settings_gpxExportNoContacts": "Немає контактів для експорту.",
|
||||
"settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі",
|
||||
"settings_gpxExportError": "Сталася помилка під час експорту.",
|
||||
"settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.",
|
||||
"settings_gpxExportAll": "Експортувати всі контакти до GPX",
|
||||
"settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.",
|
||||
"settings_gpxExportContacts": "Експортувати супутників до GPX",
|
||||
"settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати",
|
||||
"settings_gpxExportChat": "Місця супутників",
|
||||
"settings_gpxExportError": "Сталась помилка під час експорту.",
|
||||
"settings_gpxExportAllSubtitle": "Експортує всі контакти з геопозицією у файл GPX.",
|
||||
"settings_gpxExportAll": "Експорт усіх контактів у GPX",
|
||||
"settings_gpxExportContactsSubtitle": "Експортує контакти з геопозицією у файл GPX.",
|
||||
"settings_gpxExportContacts": "Експорт контактів у GPX",
|
||||
"settings_gpxExportRepeatersRoom": "Геопозиції ретрансляторів та серверів кімнат",
|
||||
"settings_gpxExportChat": "Геопозиції контактів",
|
||||
"settings_gpxExportShareText": "Дані карти експортовані з meshcore-open",
|
||||
"settings_gpxExportAllContacts": "Усі місця контактів",
|
||||
"settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX",
|
||||
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!",
|
||||
"pathTrace_someHopsNoLocation": "Один або декілька переходів не мають даних про геопозицію!",
|
||||
"map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху",
|
||||
"map_runTrace": "Виконати трасування шляху",
|
||||
"pathTrace_clearTooltip": "Очистити шлях",
|
||||
"map_removeLast": "Видалити останній",
|
||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||
"map_pathTraceCancelled": "Трасування шляху скасовано.",
|
||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||
"scanner_chromeRequired": "Потрібен браузер Chrome",
|
||||
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
|
||||
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому вебзастосунку потрібен Google Chrome або браузер на базі Chromium.",
|
||||
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||
"snrIndicator_nearByRepeaters": "Найближчі ретранслятори",
|
||||
"chat_ShowAllPaths": "Показати всі шляхи",
|
||||
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
||||
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
||||
"settings_clientRepeat": "Автономна система",
|
||||
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "одиниці",
|
||||
"appSettings_unitsMetric": "Метричний (м / км)",
|
||||
"appSettings_unitsImperial": "Імперська (ft / mi)",
|
||||
"appSettings_unitsTitle": "Одиниці",
|
||||
"appSettings_unitsMetric": "Метричні (м / км)",
|
||||
"appSettings_unitsImperial": "Імперські (ft / mi)",
|
||||
"map_lineOfSight": "Пряма видимість",
|
||||
"map_losScreenTitle": "Пряма видимість",
|
||||
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
|
||||
@@ -1636,7 +1647,7 @@
|
||||
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
|
||||
"losShowDisplayNodes": "Показати вузли відображення",
|
||||
"losCustomPoints": "Користувальницькі точки",
|
||||
"losCustomPointLabel": "Спеціальний {index}",
|
||||
"losCustomPointLabel": "Власна точка {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
@@ -1725,7 +1736,7 @@
|
||||
},
|
||||
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
|
||||
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
|
||||
"losRenameCustomPoint": "Перейменуйте спеціальну точку",
|
||||
"losRenameCustomPoint": "Перейменувати власну точку",
|
||||
"losPointName": "Назва точки",
|
||||
"losShowPanelTooltip": "Показати панель LOS",
|
||||
"losHidePanelTooltip": "Приховати панель LOS",
|
||||
@@ -1805,19 +1816,19 @@
|
||||
"contacts_unread": "Непрочитане",
|
||||
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
|
||||
"settings_contactSettings": "Налаштування контактів",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлених користувачів",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати ретранслятори",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені ретранслятори",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
|
||||
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
|
||||
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
|
||||
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
|
||||
"contactsSettings_title": "Налаштування контактів",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сервери кімнат.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати сенсори",
|
||||
"discoveredContacts_searchHint": "Знайти виявлені контакти",
|
||||
"discoveredContacts_contactAdded": "Контакт додано",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Дозволити пристрою-компаньйону автоматично додавати виявлені сенсори",
|
||||
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
|
||||
"discoveredContacts_Title": "Виявлені контакти",
|
||||
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
|
||||
@@ -1828,9 +1839,9 @@
|
||||
"common_deleteAll": "Видалити все",
|
||||
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
|
||||
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
|
||||
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
|
||||
"map_guessedLocation": "Визначено місцезнаходження",
|
||||
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||
"map_showGuessedLocations": "Показати геопозиції передбачених вузлів",
|
||||
"map_guessedLocation": "Передбачена геопозиція",
|
||||
"usbScreenSubtitle": "Виберіть виявлений USB-пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||
"usbScreenTitle": "Підключити через USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
@@ -1842,8 +1853,8 @@
|
||||
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
|
||||
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
|
||||
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
|
||||
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
|
||||
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
|
||||
"usbErrorOpenFailed": "Не вдалось відкрити вибране USB-пристрій.",
|
||||
"usbErrorConnectFailed": "Не вдалось підключитись до вибраного USB-пристрою.",
|
||||
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
|
||||
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
||||
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
||||
@@ -1858,9 +1869,9 @@
|
||||
},
|
||||
"usbStatus_searching": "Пошук пристроїв USB...",
|
||||
"usbStatus_notConnected": "Виберіть пристрій USB",
|
||||
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
||||
"usbConnectionFailed": "Не вдалось встановити з'єднання через USB: {error}",
|
||||
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
||||
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||
"usbErrorConnectTimedOut": "З'єднання не вдалось встановити. Переконайтесь, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
@@ -1878,18 +1889,18 @@
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpHostLabel": "IP-адреса",
|
||||
"tcpScreenTitle": "З'єднатися через протокол TCP",
|
||||
"tcpScreenTitle": "Підключитись через TCP",
|
||||
"tcpPortLabel": "Порт",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Введіть кінцеву точку та підключіться",
|
||||
"tcpStatus_connectingTo": "Підключення до {endpoint}...",
|
||||
"tcpErrorHostRequired": "Необхідно вказати IP-адресу.",
|
||||
"tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.",
|
||||
"tcpErrorPortInvalid": "Порт має бути в межах від 1 до 65535.",
|
||||
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
|
||||
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
|
||||
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Показати контакти Відкриття",
|
||||
"map_setAsMyLocation": "Встановити моє місцезнаходження",
|
||||
"tcpErrorTimedOut": "З'єднання TCP завершилось через закінчення часу очікування.",
|
||||
"tcpConnectionFailed": "Не вдалось встановити з'єднання TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Показати виявлені контакти",
|
||||
"map_setAsMyLocation": "Встановити мою геопозицію",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
@@ -1900,12 +1911,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
|
||||
"settings_privacySubtitle": "Керуйте інформацією, яка буде спільно використовуватись",
|
||||
"settings_privacy": "Налаштування приватності",
|
||||
"settings_telemetryBaseMode": "Режим базової телеметрії",
|
||||
"settings_telemetryLocationMode": "Режим місця телеметрії",
|
||||
"settings_advertLocation": "Розміщення реклами",
|
||||
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
|
||||
"settings_advertLocation": "Геопозиція в оголошенні",
|
||||
"settings_advertLocationSubtitle": "Включити геопозицію в оголошення",
|
||||
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
|
||||
"settings_allowAll": "Дозволити все",
|
||||
"settings_denyAll": "Відхилити все",
|
||||
@@ -1913,8 +1924,8 @@
|
||||
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
|
||||
"contact_info": "Контактна інформація",
|
||||
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
|
||||
"contact_teleLoc": "Розташування телеметрії",
|
||||
"contact_teleBase": "Базовий телебачення",
|
||||
"contact_teleLoc": "Геопозиція телеметрії",
|
||||
"contact_teleBase": "Базова телеметрія",
|
||||
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
|
||||
"contact_settings": "Налаштування контактів",
|
||||
"contact_telemetry": "Телеметрія",
|
||||
@@ -1922,13 +1933,6 @@
|
||||
"contact_lastSeen": "Останній раз бачили",
|
||||
"contact_teleEnv": "Середовище телеметрії",
|
||||
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Початкова вартість маршруту",
|
||||
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
|
||||
"appSettings_maxRouteWeight": "Максимальна вага маршруту",
|
||||
@@ -1941,9 +1945,8 @@
|
||||
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
|
||||
"settings_multiAck": "Багатократне підтвердження: {value}",
|
||||
"map_showOverlaps": "Перекриття ключа повторювача",
|
||||
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом",
|
||||
"map_showOverlaps": "Перекриття ключів ретрансляторів",
|
||||
"map_runTraceWithReturnPath": "Повернутись назад тим же шляхом",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
@@ -1987,26 +1990,54 @@
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.",
|
||||
"appSettings_languageHu": "Угорський",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.",
|
||||
"appSettings_languageHu": "Угорська",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з непрочитаними повідомленнями, прокрутіть до першого непрочитаного повідомлення, а не до останнього.",
|
||||
"appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення",
|
||||
"appSettings_languageJa": "Японська",
|
||||
"appSettings_languageKo": "Кореєська",
|
||||
"appSettings_languageKo": "Корейська",
|
||||
"radioStats_tooltip": "Статистика радіо та мережі",
|
||||
"radioStats_screenTitle": "Дані про радіостанції",
|
||||
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.",
|
||||
"radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.",
|
||||
"radioStats_waiting": "Очікую на отримання даних…",
|
||||
"radioStats_screenTitle": "Статистика радіо",
|
||||
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіо.",
|
||||
"radioStats_firmwareTooOld": "Статистика радіо вимагає прошивки пристрою-компаньйона версії 8 або новішої.",
|
||||
"radioStats_waiting": "Очікування даних…",
|
||||
"radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм",
|
||||
"radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм",
|
||||
"radioStats_lastSnr": "Останній показник SNR: {snr} дБ",
|
||||
"radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд",
|
||||
"radioStats_rxAir": "Загальний час використання RX: {seconds} секунд",
|
||||
"radioStats_txAir": "Час в ефірі TX (загальний): {seconds} секунд",
|
||||
"radioStats_rxAir": "Час в ефірі RX (загальний): {seconds} секунд",
|
||||
"radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.",
|
||||
"radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм",
|
||||
"radioStats_stripWaiting": "Отримано статистику радіо…",
|
||||
"radioStats_settingsTile": "Дані про радіостанції",
|
||||
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.",
|
||||
"radioStats_settingsTile": "Статистика радіо",
|
||||
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час в ефірі.",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Перекладіть перед відправкою",
|
||||
"translation_title": "Переклад",
|
||||
"translation_enableTitle": "Увімкнути переклад",
|
||||
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
|
||||
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
|
||||
"translation_targetLanguage": "Цільова мова",
|
||||
"translation_useAppLanguage": "Використовувати мову застосунку",
|
||||
"translation_downloadedModelLabel": "Завантажений шаблон",
|
||||
"translation_presetModelLabel": "Попередньо налаштована модель з Hugging Face",
|
||||
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
|
||||
"translation_downloadModel": "Завантажити модель",
|
||||
"translation_downloading": "Завантаження...",
|
||||
"translation_working": "Працюю...",
|
||||
"translation_stop": "Припинити",
|
||||
"translation_mergingChunks": "Об'єднання завантажених фрагментів у кінцевий файл...",
|
||||
"translation_downloadedModels": "Завантажені моделі",
|
||||
"translation_deleteModel": "Видалити модель",
|
||||
"translation_modelDownloaded": "Модель перекладу завантажена.",
|
||||
"translation_downloadStopped": "Завантаження призупинено.",
|
||||
"translation_downloadFailed": "Не вдалось завантажити: {error}",
|
||||
"translation_enterUrlFirst": "Спочатку введіть URL моделі.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2014,8 +2045,88 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerEnabledHint": "Повідомлення будуть перекладені перед відправленням.",
|
||||
"translation_messageTranslation": "Переклад повідомлення",
|
||||
"translation_composerDisabledHint": "Надсилайте повідомлення, використовуючи оригінальний текстовий формат.",
|
||||
"translation_translateBeforeSending": "Перекладіть перед відправкою",
|
||||
"translation_translateTo": "Перекласти на {language}",
|
||||
"translation_translationOptions": "Варіанти перекладу",
|
||||
"translation_systemLanguage": "Мова системи",
|
||||
"scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
|
||||
"scanner_linuxPairingShowPin": "Показати PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
|
||||
"scanner_linuxPairingHidePin": "Приховати PIN"
|
||||
"scanner_linuxPairingHidePin": "Приховати PIN",
|
||||
"repeater_cliQuickClockSync": "Синхронізація годинника",
|
||||
"repeater_cliQuickDiscovery": "Виявити сусідів",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
|
||||
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
|
||||
"repeater_guestTools": "Гостьові інструменти",
|
||||
"repeater_guest": "Інформація про ретранслятор",
|
||||
"room_guest": "Інформація про сервер кімнати",
|
||||
"chat_sendMessage": "Надіслати повідомлення",
|
||||
"background_serviceTitle": "MeshCore працює",
|
||||
"background_serviceText": "Підтримує з'єднання BLE",
|
||||
"appSettings_translationModelDeleted": "Видалено {name}",
|
||||
"appSettings_translationModelDeleteFailed": "Не вдалось видалити: {error}",
|
||||
"channels_channelUpdateFailed": "Не вдалось оновити канал: {error}",
|
||||
"contact_typeChat": "Чат",
|
||||
"contact_typeRepeater": "Ретранслятор",
|
||||
"contact_typeRoom": "Кімната",
|
||||
"contact_typeSensor": "Сенсор",
|
||||
"contact_typeUnknown": "Невідомо",
|
||||
"settings_multiAck": "Багато підтверджень",
|
||||
"map_sharedAt": "Поділено",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsHint": "Натисніть на заблоковане місце, щоб виділити його на карті.",
|
||||
"losBlockedSpotsTitle": "Заблоковані місця",
|
||||
"losSelectedObstructionTitle": "Вибраний об'єкт перешкоди",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
+143
-11
@@ -109,6 +109,8 @@
|
||||
"settings_privacyModeEnabled": "隐私模式已启用",
|
||||
"settings_privacyModeDisabled": "隐私模式已关闭",
|
||||
"settings_actions": "操作",
|
||||
"settings_deleteAllPaths": "Delete All Paths",
|
||||
"settings_deleteAllPathsSubtitle": "Clear all path data from contacts.",
|
||||
"settings_sendAdvertisement": "发送广播",
|
||||
"settings_sendAdvertisementSubtitle": "立即发送广播",
|
||||
"settings_advertisementSent": "已发送广播",
|
||||
@@ -1927,13 +1929,6 @@
|
||||
"contact_settings": "联系人设置",
|
||||
"contact_teleLocSubtitle": "允许共享位置数据",
|
||||
"contact_telemetry": "遥测数据",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "最大路径重量",
|
||||
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
|
||||
"appSettings_initialRouteWeight": "初始路线权重",
|
||||
@@ -1945,7 +1940,7 @@
|
||||
"appSettings_maxMessageRetries": "最大消息重试次数",
|
||||
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "多重ACK:{value}",
|
||||
"settings_multiAck": "多重ACK",
|
||||
"settings_telemetryModeUpdated": "遥测模式已更新",
|
||||
"map_showOverlaps": "重复键重叠",
|
||||
"map_runTraceWithReturnPath": "沿着相同的路径返回",
|
||||
@@ -2012,6 +2007,34 @@
|
||||
"radioStats_stripWaiting": "正在获取收音机数据…",
|
||||
"radioStats_settingsTile": "广播统计数据",
|
||||
"radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "翻译",
|
||||
"translation_enableSubtitle": "翻译收到的消息,并允许在发送前进行翻译。",
|
||||
"translation_composerTitle": "在发送之前进行翻译",
|
||||
"translation_enableTitle": "启用翻译功能",
|
||||
"translation_composerSubtitle": "控制作曲家翻译图标的默认状态。",
|
||||
"translation_targetLanguage": "目标语言",
|
||||
"translation_useAppLanguage": "使用应用程序语言",
|
||||
"translation_downloadedModelLabel": "下载的模型",
|
||||
"translation_presetModelLabel": "预设的 Hugging Face 模型",
|
||||
"translation_downloadModel": "下载模型",
|
||||
"translation_manualUrlLabel": "手动模型网址",
|
||||
"translation_downloading": "正在下载...",
|
||||
"translation_working": "工作中...",
|
||||
"translation_stop": "停止",
|
||||
"translation_mergingChunks": "将下载的片段合并成最终文件...",
|
||||
"translation_downloadedModels": "下载的模型",
|
||||
"translation_deleteModel": "删除模型",
|
||||
"translation_modelDownloaded": "翻译模型已下载。",
|
||||
"translation_downloadStopped": "下载已停止。",
|
||||
"translation_downloadFailed": "下载失败:{error}",
|
||||
"translation_enterUrlFirst": "首先,请输入模型的 URL。",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -2019,8 +2042,117 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingShowPin": "显示 PIN码",
|
||||
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN(如果没有,请留空)。",
|
||||
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
|
||||
"scanner_linuxPairingHidePin": "隐藏 PIN"
|
||||
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。",
|
||||
"scanner_linuxPairingHidePin": "隐藏 PIN",
|
||||
"scanner_linuxPairingShowPin": "显示PIN码",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "使用原始的打字方式发送消息。",
|
||||
"translation_messageTranslation": "消息翻译",
|
||||
"translation_composerEnabledHint": "消息将在发送前进行翻译。",
|
||||
"translation_translateBeforeSending": "在发送前进行翻译",
|
||||
"translation_translateTo": "翻译成 {language}",
|
||||
"translation_translationOptions": "翻译选项",
|
||||
"translation_systemLanguage": "系统语言",
|
||||
"repeater_cliQuickDiscovery": "发现邻居",
|
||||
"repeater_cliQuickClockSync": "同步时钟",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "登录后,自动同步时钟",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。",
|
||||
"repeater_guestTools": "访客工具",
|
||||
"repeater_guest": "重复器信息",
|
||||
"chat_sendMessage": "发送消息",
|
||||
"room_guest": "服务器信息",
|
||||
"common_done": "Done",
|
||||
"background_serviceTitle": "MeshCore running",
|
||||
"background_serviceText": "Keeping BLE connected",
|
||||
"appSettings_translationModelDeleted": "Deleted {name}",
|
||||
"@appSettings_translationModelDeleted": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_translationModelDeleteFailed": "Failed to delete: {error}",
|
||||
"@appSettings_translationModelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_channelUpdateFailed": "Failed to update channel: {error}",
|
||||
"@channels_channelUpdateFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_type": "Type",
|
||||
"map_path": "Path",
|
||||
"map_location": "Location",
|
||||
"map_estLocation": "Est. Location",
|
||||
"map_publicKey": "Public Key",
|
||||
"map_publicKeyPrefixHint": "e.g. ab12",
|
||||
"contact_typeChat": "Chat",
|
||||
"contact_typeRepeater": "Repeater",
|
||||
"contact_typeRoom": "Room",
|
||||
"contact_typeSensor": "Sensor",
|
||||
"contact_typeUnknown": "Unknown",
|
||||
"channels_via": "via {path}",
|
||||
"chat_score": "Score",
|
||||
"map_sharedAt": "已分享",
|
||||
"@losBlockedSpotChip": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@losSelectedObstructionDetails": {
|
||||
"placeholders": {
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromA": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceFromB": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losBlockedSpotsTitle": "被占用区域",
|
||||
"losBlockedSpotsHint": "点击地图上的某个被遮盖的区域,以突出显示该区域。",
|
||||
"losSelectedObstructionTitle": "选择性阻碍",
|
||||
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
|
||||
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'app_localizations.dart';
|
||||
|
||||
/// UI-level localization helpers for [Contact].
|
||||
///
|
||||
/// Kept out of the model layer so `Contact` does not depend on
|
||||
/// `AppLocalizations`. Use these from widgets/screens; for logs and
|
||||
/// non-UI export use `Contact.typeLabelRaw`.
|
||||
extension ContactLocalization on Contact {
|
||||
String typeLabel(AppLocalizations l10n) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return l10n.contact_typeChat;
|
||||
case advTypeRepeater:
|
||||
return l10n.contact_typeRepeater;
|
||||
case advTypeRoom:
|
||||
return l10n.contact_typeRoom;
|
||||
case advTypeSensor:
|
||||
return l10n.contact_typeSensor;
|
||||
default:
|
||||
return l10n.contact_typeUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
String pathLabel(AppLocalizations l10n) {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return l10n.chat_floodForced;
|
||||
if (pathOverride == 0) return l10n.chat_directForced;
|
||||
return l10n.chat_hopsForced(pathOverride!);
|
||||
}
|
||||
if (pathLength < 0) return l10n.channelPath_floodPath;
|
||||
if (pathLength == 0) return l10n.chat_direct;
|
||||
return l10n.chat_hopsCount(pathLength);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart';
|
||||
import 'services/background_service.dart';
|
||||
import 'services/map_tile_cache_service.dart';
|
||||
import 'services/chat_text_scale_service.dart';
|
||||
import 'services/translation_service.dart';
|
||||
import 'services/ui_view_state_service.dart';
|
||||
import 'services/timeout_prediction_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
@@ -41,6 +42,7 @@ void main() async {
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
final translationService = TranslationService(appSettingsService);
|
||||
final uiViewStateService = UiViewStateService();
|
||||
final timeoutPredictionService = TimeoutPredictionService(storage);
|
||||
|
||||
@@ -57,9 +59,13 @@ void main() async {
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.initialize();
|
||||
await backgroundService.initialize();
|
||||
backgroundService.setLanguageOverrideProvider(
|
||||
() => appSettingsService.settings.languageOverride,
|
||||
);
|
||||
_registerThirdPartyLicenses();
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
await translationService.refreshDownloadedModels();
|
||||
await uiViewStateService.initialize();
|
||||
await timeoutPredictionService.initialize();
|
||||
|
||||
@@ -68,6 +74,7 @@ void main() async {
|
||||
retryService: retryService,
|
||||
pathHistoryService: pathHistoryService,
|
||||
appSettingsService: appSettingsService,
|
||||
translationService: translationService,
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
backgroundService: backgroundService,
|
||||
@@ -93,6 +100,7 @@ void main() async {
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
chatTextScaleService: chatTextScaleService,
|
||||
translationService: translationService,
|
||||
uiViewStateService: uiViewStateService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
),
|
||||
@@ -130,6 +138,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final AppDebugLogService appDebugLogService;
|
||||
final MapTileCacheService mapTileCacheService;
|
||||
final ChatTextScaleService chatTextScaleService;
|
||||
final TranslationService translationService;
|
||||
final UiViewStateService uiViewStateService;
|
||||
final TimeoutPredictionService timeoutPredictionService;
|
||||
|
||||
@@ -144,6 +153,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.appDebugLogService,
|
||||
required this.mapTileCacheService,
|
||||
required this.chatTextScaleService,
|
||||
required this.translationService,
|
||||
required this.uiViewStateService,
|
||||
required this.timeoutPredictionService,
|
||||
});
|
||||
@@ -159,6 +169,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||
ChangeNotifierProvider.value(value: translationService),
|
||||
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||
Provider.value(value: storage),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'translation_support.dart';
|
||||
|
||||
enum UnitSystem { metric, imperial }
|
||||
|
||||
extension UnitSystemValue on UnitSystem {
|
||||
@@ -49,6 +51,12 @@ class AppSettings {
|
||||
final String tcpServerAddress;
|
||||
final int tcpServerPort;
|
||||
final bool jumpToOldestUnread;
|
||||
final bool translationEnabled;
|
||||
final String? translationTargetLanguageCode;
|
||||
final bool composerTranslationEnabled;
|
||||
final String? translationModelSourceUrl;
|
||||
final String? translationSelectedModelId;
|
||||
final List<TranslationModelRecord> translationDownloadedModels;
|
||||
|
||||
AppSettings({
|
||||
this.clearPathOnMaxRetry = false,
|
||||
@@ -86,9 +94,16 @@ class AppSettings {
|
||||
this.tcpServerAddress = '',
|
||||
this.tcpServerPort = 0,
|
||||
this.jumpToOldestUnread = false,
|
||||
this.translationEnabled = false,
|
||||
this.translationTargetLanguageCode,
|
||||
this.composerTranslationEnabled = false,
|
||||
this.translationModelSourceUrl,
|
||||
this.translationSelectedModelId,
|
||||
List<TranslationModelRecord>? translationDownloadedModels,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {};
|
||||
mutedChannels = mutedChannels ?? {},
|
||||
translationDownloadedModels = translationDownloadedModels ?? const [];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
@@ -127,6 +142,14 @@ class AppSettings {
|
||||
'tcp_server_address': tcpServerAddress,
|
||||
'tcp_server_port': tcpServerPort,
|
||||
'jump_to_oldest_unread': jumpToOldestUnread,
|
||||
'translation_enabled': translationEnabled,
|
||||
'translation_target_language_code': translationTargetLanguageCode,
|
||||
'composer_translation_enabled': composerTranslationEnabled,
|
||||
'translation_model_source_url': translationModelSourceUrl,
|
||||
'translation_selected_model_id': translationSelectedModelId,
|
||||
'translation_downloaded_models': translationDownloadedModels
|
||||
.map((model) => model.toJson())
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,6 +219,24 @@ class AppSettings {
|
||||
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
||||
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
|
||||
translationEnabled: json['translation_enabled'] as bool? ?? false,
|
||||
translationTargetLanguageCode:
|
||||
json['translation_target_language_code'] as String?,
|
||||
composerTranslationEnabled:
|
||||
json['composer_translation_enabled'] as bool? ?? false,
|
||||
translationModelSourceUrl:
|
||||
json['translation_model_source_url'] as String?,
|
||||
translationSelectedModelId:
|
||||
json['translation_selected_model_id'] as String?,
|
||||
translationDownloadedModels:
|
||||
(json['translation_downloaded_models'] as List<dynamic>?)
|
||||
?.map(
|
||||
(entry) => TranslationModelRecord.fromJson(
|
||||
Map<String, dynamic>.from(entry as Map),
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,6 +276,12 @@ class AppSettings {
|
||||
String? tcpServerAddress,
|
||||
int? tcpServerPort,
|
||||
bool? jumpToOldestUnread,
|
||||
bool? translationEnabled,
|
||||
Object? translationTargetLanguageCode = _unset,
|
||||
bool? composerTranslationEnabled,
|
||||
Object? translationModelSourceUrl = _unset,
|
||||
Object? translationSelectedModelId = _unset,
|
||||
List<TranslationModelRecord>? translationDownloadedModels,
|
||||
}) {
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||
@@ -284,6 +331,20 @@ class AppSettings {
|
||||
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
|
||||
translationEnabled: translationEnabled ?? this.translationEnabled,
|
||||
translationTargetLanguageCode: translationTargetLanguageCode == _unset
|
||||
? this.translationTargetLanguageCode
|
||||
: translationTargetLanguageCode as String?,
|
||||
composerTranslationEnabled:
|
||||
composerTranslationEnabled ?? this.composerTranslationEnabled,
|
||||
translationModelSourceUrl: translationModelSourceUrl == _unset
|
||||
? this.translationModelSourceUrl
|
||||
: translationModelSourceUrl as String?,
|
||||
translationSelectedModelId: translationSelectedModelId == _unset
|
||||
? this.translationSelectedModelId
|
||||
: translationSelectedModelId as String?,
|
||||
translationDownloadedModels:
|
||||
translationDownloadedModels ?? this.translationDownloadedModels,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import 'translation_support.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
enum ChannelMessageStatus { pending, sent, failed }
|
||||
@@ -24,9 +25,16 @@ class Repeat {
|
||||
}
|
||||
|
||||
class ChannelMessage {
|
||||
static const Object _unset = Object();
|
||||
|
||||
final Uint8List? senderKey;
|
||||
final String senderName;
|
||||
final String text;
|
||||
final String? originalText;
|
||||
final String? translatedText;
|
||||
final String? translatedLanguageCode;
|
||||
final MessageTranslationStatus translationStatus;
|
||||
final String? translationModelId;
|
||||
final DateTime timestamp;
|
||||
final bool isOutgoing;
|
||||
final ChannelMessageStatus status;
|
||||
@@ -47,6 +55,11 @@ class ChannelMessage {
|
||||
this.senderKey,
|
||||
required this.senderName,
|
||||
required this.text,
|
||||
this.originalText,
|
||||
this.translatedText,
|
||||
this.translatedLanguageCode,
|
||||
this.translationStatus = MessageTranslationStatus.none,
|
||||
this.translationModelId,
|
||||
required this.timestamp,
|
||||
required this.isOutgoing,
|
||||
this.status = ChannelMessageStatus.pending,
|
||||
@@ -86,12 +99,30 @@ class ChannelMessage {
|
||||
String? replyToMessageId,
|
||||
String? replyToSenderName,
|
||||
String? replyToText,
|
||||
Object? originalText = _unset,
|
||||
Object? translatedText = _unset,
|
||||
Object? translatedLanguageCode = _unset,
|
||||
MessageTranslationStatus? translationStatus,
|
||||
Object? translationModelId = _unset,
|
||||
Map<String, int>? reactions,
|
||||
}) {
|
||||
return ChannelMessage(
|
||||
senderKey: senderKey,
|
||||
senderName: senderName,
|
||||
text: text,
|
||||
originalText: originalText == _unset
|
||||
? this.originalText
|
||||
: originalText as String?,
|
||||
translatedText: translatedText == _unset
|
||||
? this.translatedText
|
||||
: translatedText as String?,
|
||||
translatedLanguageCode: translatedLanguageCode == _unset
|
||||
? this.translatedLanguageCode
|
||||
: translatedLanguageCode as String?,
|
||||
translationStatus: translationStatus ?? this.translationStatus,
|
||||
translationModelId: translationModelId == _unset
|
||||
? this.translationModelId
|
||||
: translationModelId as String?,
|
||||
timestamp: timestamp,
|
||||
isOutgoing: isOutgoing,
|
||||
status: status ?? this.status,
|
||||
@@ -191,12 +222,18 @@ class ChannelMessage {
|
||||
static ChannelMessage outgoing(
|
||||
String text,
|
||||
String senderName,
|
||||
int channelIndex,
|
||||
) {
|
||||
int channelIndex, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
}) {
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
timestamp: DateTime.now(),
|
||||
isOutgoing: true,
|
||||
status: ChannelMessageStatus.pending,
|
||||
|
||||
+32
-15
@@ -17,6 +17,7 @@ class Contact {
|
||||
final double? longitude;
|
||||
final DateTime lastSeen;
|
||||
final DateTime lastMessageAt;
|
||||
final DateTime? lastModified;
|
||||
final bool isActive;
|
||||
final bool wasPulled;
|
||||
final Uint8List? rawPacket;
|
||||
@@ -33,6 +34,7 @@ class Contact {
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.lastSeen,
|
||||
this.lastModified,
|
||||
DateTime? lastMessageAt,
|
||||
this.isActive = true,
|
||||
this.wasPulled = false,
|
||||
@@ -41,7 +43,10 @@ class Contact {
|
||||
|
||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||
|
||||
String get typeLabel {
|
||||
/// Non-localized type label, intended for logs and non-UI exports
|
||||
/// (e.g. GPX). For UI use the `typeLabel(l10n)` extension in
|
||||
/// `lib/l10n/contact_localization.dart`.
|
||||
String get typeLabelRaw {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return 'Chat';
|
||||
@@ -56,17 +61,6 @@ class Contact {
|
||||
}
|
||||
}
|
||||
|
||||
String get pathLabel {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return 'Flood (forced)';
|
||||
if (pathOverride == 0) return 'Direct (forced)';
|
||||
return '$pathOverride hops (forced)';
|
||||
}
|
||||
if (pathLength < 0) return 'Flood';
|
||||
if (pathLength == 0) return 'Direct';
|
||||
return '$pathLength hops';
|
||||
}
|
||||
|
||||
bool get hasLocation {
|
||||
const double epsilon = 1e-6;
|
||||
final lat = latitude ?? 0.0;
|
||||
@@ -94,6 +88,7 @@ class Contact {
|
||||
double? longitude,
|
||||
DateTime? lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
DateTime? lastModified,
|
||||
bool? isActive,
|
||||
Uint8List? rawPacket,
|
||||
}) {
|
||||
@@ -114,6 +109,7 @@ class Contact {
|
||||
longitude: longitude ?? this.longitude,
|
||||
lastSeen: lastSeen ?? this.lastSeen,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
isActive: isActive ?? this.isActive,
|
||||
rawPacket: rawPacket ?? this.rawPacket,
|
||||
);
|
||||
@@ -182,16 +178,34 @@ class Contact {
|
||||
return null;
|
||||
}
|
||||
|
||||
final lastMod = reader.readUInt32LE();
|
||||
// mandatory last_advert_timestamp
|
||||
final lastAdvertTimestamp = reader.readUInt32LE();
|
||||
|
||||
double? lat, lon;
|
||||
if (reader.remaining >= 8) {
|
||||
DateTime? lastModified;
|
||||
if (reader.remaining >= 12) {
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
final lastModRaw = reader.readUInt32LE();
|
||||
// TODO: should this be &&?
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
if (lastModRaw != 0) {
|
||||
lastModified = DateTime.fromMillisecondsSinceEpoch(lastModRaw * 1000);
|
||||
}
|
||||
} else if (reader.remaining >= 8) {
|
||||
// Old layout: gps without lastmod
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
appLogger.info(
|
||||
'Contact ${pubKeyToHex(pubKey).substring(0, 8)} has gps but no lastmod (legacy firmware layout)',
|
||||
);
|
||||
}
|
||||
|
||||
return Contact(
|
||||
@@ -203,7 +217,10 @@ class Contact {
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastAdvertTimestamp * 1000,
|
||||
),
|
||||
lastModified: lastModified,
|
||||
isActive: true,
|
||||
rawPacket: null,
|
||||
);
|
||||
|
||||
+43
-3
@@ -1,19 +1,27 @@
|
||||
import 'dart:typed_data';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import 'translation_support.dart';
|
||||
|
||||
enum MessageStatus { pending, sent, delivered, failed }
|
||||
|
||||
class Message {
|
||||
static const Object _unset = Object();
|
||||
|
||||
final Uint8List senderKey;
|
||||
final String text;
|
||||
final DateTime timestamp;
|
||||
final bool isOutgoing;
|
||||
final bool isCli;
|
||||
final MessageStatus status;
|
||||
final String? originalText;
|
||||
final String? translatedText;
|
||||
final String? translatedLanguageCode;
|
||||
final MessageTranslationStatus translationStatus;
|
||||
final String? translationModelId;
|
||||
|
||||
// NEW: Retry logic fields
|
||||
final String? messageId;
|
||||
final String messageId;
|
||||
final int retryCount;
|
||||
final int? estimatedTimeoutMs;
|
||||
final int? expectedAckHash;
|
||||
@@ -33,7 +41,12 @@ class Message {
|
||||
required this.isOutgoing,
|
||||
this.isCli = false,
|
||||
this.status = MessageStatus.pending,
|
||||
this.messageId,
|
||||
String? messageId,
|
||||
this.originalText,
|
||||
this.translatedText,
|
||||
this.translatedLanguageCode,
|
||||
this.translationStatus = MessageTranslationStatus.none,
|
||||
this.translationModelId,
|
||||
this.retryCount = 0,
|
||||
this.estimatedTimeoutMs,
|
||||
this.expectedAckHash,
|
||||
@@ -45,7 +58,10 @@ class Message {
|
||||
Uint8List? fourByteRoomContactKey,
|
||||
Map<String, int>? reactions,
|
||||
Map<String, MessageStatus>? reactionStatuses,
|
||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
||||
}) : messageId =
|
||||
messageId ??
|
||||
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
|
||||
pathBytes = pathBytes ?? Uint8List(0),
|
||||
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
||||
reactions = reactions ?? {},
|
||||
reactionStatuses = reactionStatuses ?? {};
|
||||
@@ -63,6 +79,11 @@ class Message {
|
||||
int? pathLength,
|
||||
Uint8List? pathBytes,
|
||||
bool? isCli,
|
||||
Object? originalText = _unset,
|
||||
Object? translatedText = _unset,
|
||||
Object? translatedLanguageCode = _unset,
|
||||
MessageTranslationStatus? translationStatus,
|
||||
Object? translationModelId = _unset,
|
||||
Map<String, int>? reactions,
|
||||
Map<String, MessageStatus>? reactionStatuses,
|
||||
Uint8List? fourByteRoomContactKey,
|
||||
@@ -75,6 +96,19 @@ class Message {
|
||||
isCli: isCli ?? this.isCli,
|
||||
status: status ?? this.status,
|
||||
messageId: messageId,
|
||||
originalText: originalText == _unset
|
||||
? this.originalText
|
||||
: originalText as String?,
|
||||
translatedText: translatedText == _unset
|
||||
? this.translatedText
|
||||
: translatedText as String?,
|
||||
translatedLanguageCode: translatedLanguageCode == _unset
|
||||
? this.translatedLanguageCode
|
||||
: translatedLanguageCode as String?,
|
||||
translationStatus: translationStatus ?? this.translationStatus,
|
||||
translationModelId: translationModelId == _unset
|
||||
? this.translationModelId
|
||||
: translationModelId as String?,
|
||||
retryCount: retryCount ?? this.retryCount,
|
||||
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
|
||||
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
|
||||
@@ -124,12 +158,18 @@ class Message {
|
||||
static Message outgoing(
|
||||
Uint8List recipientKey,
|
||||
String text, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
int? pathLength,
|
||||
Uint8List? pathBytes,
|
||||
}) {
|
||||
return Message(
|
||||
senderKey: recipientKey,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
timestamp: DateTime.now(),
|
||||
isOutgoing: true,
|
||||
isCli: false,
|
||||
|
||||
@@ -228,7 +228,7 @@ class RadioSettings {
|
||||
frequencyMHz: 433.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
@@ -238,7 +238,7 @@ class RadioSettings {
|
||||
frequencyMHz: 869.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
@@ -248,7 +248,7 @@ class RadioSettings {
|
||||
frequencyMHz: 918.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
enum MessageTranslationStatus { none, pending, completed, failed, skipped }
|
||||
|
||||
extension MessageTranslationStatusValue on MessageTranslationStatus {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case MessageTranslationStatus.pending:
|
||||
return 'pending';
|
||||
case MessageTranslationStatus.completed:
|
||||
return 'completed';
|
||||
case MessageTranslationStatus.failed:
|
||||
return 'failed';
|
||||
case MessageTranslationStatus.skipped:
|
||||
return 'skipped';
|
||||
case MessageTranslationStatus.none:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageTranslationStatus parseMessageTranslationStatus(dynamic value) {
|
||||
if (value is! String) {
|
||||
return MessageTranslationStatus.none;
|
||||
}
|
||||
for (final status in MessageTranslationStatus.values) {
|
||||
if (status.value == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return MessageTranslationStatus.none;
|
||||
}
|
||||
|
||||
class TranslationModelRecord {
|
||||
final String id;
|
||||
final String name;
|
||||
final String sourceUrl;
|
||||
final String localPath;
|
||||
final DateTime downloadedAt;
|
||||
final int fileSizeBytes;
|
||||
|
||||
const TranslationModelRecord({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sourceUrl,
|
||||
required this.localPath,
|
||||
required this.downloadedAt,
|
||||
required this.fileSizeBytes,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'source_url': sourceUrl,
|
||||
'local_path': localPath,
|
||||
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
|
||||
'file_size_bytes': fileSizeBytes,
|
||||
};
|
||||
}
|
||||
|
||||
factory TranslationModelRecord.fromJson(Map<String, dynamic> json) {
|
||||
return TranslationModelRecord(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
sourceUrl: json['source_url'] as String? ?? '',
|
||||
localPath: json['local_path'] as String? ?? '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['downloaded_at'] as int? ?? 0,
|
||||
),
|
||||
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String translationModelFriendlyName(TranslationModelRecord model) {
|
||||
switch (model.id) {
|
||||
case 'hy-mt1.5-1.8b-q4_k_m':
|
||||
return 'Tencent HY-MT 1.5 1.8B Q4_K_M';
|
||||
case 'hy-mt1.5-1.8b-q6_k':
|
||||
return 'Tencent HY-MT 1.5 1.8B Q6_K';
|
||||
default:
|
||||
final trimmed = model.name.trim();
|
||||
if (trimmed.endsWith('.gguf')) {
|
||||
return trimmed.substring(0, trimmed.length - 5);
|
||||
}
|
||||
return trimmed.isEmpty ? model.id : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
class TranslationLanguageOption {
|
||||
final String code;
|
||||
final String label;
|
||||
|
||||
const TranslationLanguageOption({required this.code, required this.label});
|
||||
}
|
||||
|
||||
const List<TranslationLanguageOption> supportedTranslationLanguages = [
|
||||
TranslationLanguageOption(code: 'bg', label: 'Bulgarian'),
|
||||
TranslationLanguageOption(code: 'de', label: 'German'),
|
||||
TranslationLanguageOption(code: 'en', label: 'English'),
|
||||
TranslationLanguageOption(code: 'es', label: 'Spanish'),
|
||||
TranslationLanguageOption(code: 'fr', label: 'French'),
|
||||
TranslationLanguageOption(code: 'hu', label: 'Hungarian'),
|
||||
TranslationLanguageOption(code: 'it', label: 'Italian'),
|
||||
TranslationLanguageOption(code: 'ja', label: 'Japanese'),
|
||||
TranslationLanguageOption(code: 'ko', label: 'Korean'),
|
||||
TranslationLanguageOption(code: 'nl', label: 'Dutch'),
|
||||
TranslationLanguageOption(code: 'pl', label: 'Polish'),
|
||||
TranslationLanguageOption(code: 'pt', label: 'Portuguese'),
|
||||
TranslationLanguageOption(code: 'ru', label: 'Russian'),
|
||||
TranslationLanguageOption(code: 'sk', label: 'Slovak'),
|
||||
TranslationLanguageOption(code: 'sl', label: 'Slovenian'),
|
||||
TranslationLanguageOption(code: 'sv', label: 'Swedish'),
|
||||
TranslationLanguageOption(code: 'uk', label: 'Ukrainian'),
|
||||
TranslationLanguageOption(code: 'zh', label: 'Chinese'),
|
||||
];
|
||||
|
||||
final List<TranslationModelRecord> translationPresetModels = [
|
||||
TranslationModelRecord(
|
||||
id: 'hy-mt1.5-1.8b-q4_k_m',
|
||||
name: 'HY-MT1.5-1.8B-Q4_K_M.gguf',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q4_K_M.gguf?download=true',
|
||||
localPath: '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
fileSizeBytes: 0,
|
||||
),
|
||||
TranslationModelRecord(
|
||||
id: 'hy-mt1.5-1.8b-q6_k',
|
||||
name: 'HY-MT1.5-1.8B-Q6_K.gguf',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q6_K.gguf?download=true',
|
||||
localPath: '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
fileSizeBytes: 0,
|
||||
),
|
||||
];
|
||||
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class AppDebugLogScreen extends StatelessWidget {
|
||||
const AppDebugLogScreen({super.key});
|
||||
@@ -34,8 +35,9 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.debugLog_copied)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.debugLog_copied),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'map_cache_screen.dart';
|
||||
|
||||
class AppSettingsScreen extends StatelessWidget {
|
||||
@@ -21,26 +25,46 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer2<AppSettingsService, MeshCoreConnector>(
|
||||
builder: (context, settingsService, connector, child) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAppearanceCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildNotificationsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildMessagingCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildBatteryCard(context, settingsService, connector),
|
||||
const SizedBox(height: 16),
|
||||
_buildMapSettingsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildDebugCard(context, settingsService),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child:
|
||||
Consumer3<
|
||||
AppSettingsService,
|
||||
MeshCoreConnector,
|
||||
TranslationService
|
||||
>(
|
||||
builder:
|
||||
(
|
||||
context,
|
||||
settingsService,
|
||||
connector,
|
||||
translationService,
|
||||
child,
|
||||
) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAppearanceCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildNotificationsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildMessagingCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
if (!kIsWeb) ...[
|
||||
_buildTranslationCard(
|
||||
context,
|
||||
settingsService,
|
||||
translationService,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_buildBatteryCard(context, settingsService, connector),
|
||||
const SizedBox(height: 16),
|
||||
_buildMapSettingsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildDebugCard(context, settingsService),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -128,13 +152,12 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
.requestPermissions();
|
||||
if (!granted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.appSettings_notificationPermissionDenied,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.appSettings_notificationPermissionDenied,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -143,15 +166,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
|
||||
await settingsService.setNotificationsEnabled(value);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_notificationsEnabled
|
||||
: context.l10n.appSettings_notificationsDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_notificationsEnabled
|
||||
: context.l10n.appSettings_notificationsDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -278,15 +300,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
value: settingsService.settings.clearPathOnMaxRetry,
|
||||
onChanged: (value) {
|
||||
settingsService.setClearPathOnMaxRetry(value);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_pathsWillBeCleared
|
||||
: context.l10n.appSettings_pathsWillNotBeCleared,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_pathsWillBeCleared
|
||||
: context.l10n.appSettings_pathsWillNotBeCleared,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -306,15 +327,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
value: settingsService.settings.autoRouteRotationEnabled,
|
||||
onChanged: (value) {
|
||||
settingsService.setAutoRouteRotationEnabled(value);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_autoRouteRotationEnabled
|
||||
: context.l10n.appSettings_autoRouteRotationDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_autoRouteRotationEnabled
|
||||
: context.l10n.appSettings_autoRouteRotationDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -530,6 +550,211 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTranslationCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
TranslationService translationService,
|
||||
) {
|
||||
final settings = settingsService.settings;
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
context.l10n.translation_title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.translate),
|
||||
title: Text(context.l10n.translation_enableTitle),
|
||||
subtitle: Text(context.l10n.translation_enableSubtitle),
|
||||
value: settings.translationEnabled,
|
||||
onChanged: settingsService.setTranslationEnabled,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.outgoing_mail),
|
||||
title: Text(context.l10n.translation_composerTitle),
|
||||
subtitle: Text(context.l10n.translation_composerSubtitle),
|
||||
value: settings.composerTranslationEnabled,
|
||||
onChanged: settingsService.setComposerTranslationEnabled,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: Text(context.l10n.translation_targetLanguage),
|
||||
subtitle: Text(
|
||||
_translationLanguageLabel(
|
||||
context,
|
||||
settings.translationTargetLanguageCode,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () =>
|
||||
_showTranslationLanguageDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: settings.translationSelectedModelId,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.translation_downloadedModelLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (final model in settings.translationDownloadedModels)
|
||||
DropdownMenuItem(
|
||||
value: model.id,
|
||||
child: Text(translationModelFriendlyName(model)),
|
||||
),
|
||||
],
|
||||
onChanged: settings.translationDownloadedModels.isEmpty
|
||||
? null
|
||||
: (value) {
|
||||
settingsService.setTranslationSelectedModelId(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: null,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.translation_presetModelLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (final preset in translationPresetModels)
|
||||
DropdownMenuItem(
|
||||
value: preset.sourceUrl,
|
||||
child: Text(translationModelFriendlyName(preset)),
|
||||
),
|
||||
],
|
||||
onChanged: translationService.isBusy
|
||||
? null
|
||||
: (value) async {
|
||||
if (value == null) return;
|
||||
final preset = translationPresetModels.firstWhere(
|
||||
(entry) => entry.sourceUrl == value,
|
||||
);
|
||||
await _downloadTranslationModel(
|
||||
context,
|
||||
translationService,
|
||||
settingsService,
|
||||
sourceUrl: preset.sourceUrl,
|
||||
fileName: preset.name,
|
||||
id: preset.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
_TranslationUrlField(
|
||||
initialValue: settings.translationModelSourceUrl ?? '',
|
||||
onChanged: settingsService.setTranslationModelSourceUrl,
|
||||
onDownload: translationService.isBusy
|
||||
? null
|
||||
: (url) => _downloadTranslationModel(
|
||||
context,
|
||||
translationService,
|
||||
settingsService,
|
||||
sourceUrl: url,
|
||||
),
|
||||
downloadLabel: translationService.isDownloading
|
||||
? context.l10n.translation_downloading
|
||||
: translationService.isBusy
|
||||
? context.l10n.translation_working
|
||||
: context.l10n.translation_downloadModel,
|
||||
isDownloading: translationService.isDownloading,
|
||||
onCancel: translationService.cancelDownload,
|
||||
labelText: context.l10n.translation_manualUrlLabel,
|
||||
stopLabel: context.l10n.translation_stop,
|
||||
),
|
||||
if (translationService.isDownloading) ...[
|
||||
const SizedBox(height: 12),
|
||||
LinearProgressIndicator(
|
||||
value:
|
||||
translationService.downloadFileName ==
|
||||
'Merging chunks...'
|
||||
? null
|
||||
: translationService.downloadProgress,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_downloadProgressLabel(context, translationService),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (settings.translationDownloadedModels.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
context.l10n.translation_downloadedModels,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
for (final model in settings.translationDownloadedModels)
|
||||
Card.outlined(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
leading: Icon(
|
||||
model.id == settings.translationSelectedModelId
|
||||
? Icons.check_circle
|
||||
: Icons.memory_outlined,
|
||||
),
|
||||
title: Text(translationModelFriendlyName(model)),
|
||||
subtitle: Text(_downloadedModelLabel(model)),
|
||||
trailing: IconButton(
|
||||
tooltip: context.l10n.translation_deleteModel,
|
||||
onPressed: translationService.isBusy
|
||||
? null
|
||||
: () => _deleteTranslationModel(
|
||||
context,
|
||||
translationService,
|
||||
model,
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
onTap: () => settingsService
|
||||
.setTranslationSelectedModelId(model.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (translationService.lastError != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
translationService.lastError!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Fixed rendering issues
|
||||
Widget _buildBatteryCard(
|
||||
BuildContext context,
|
||||
@@ -837,25 +1062,25 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
children: [
|
||||
Text(context.l10n.appSettings_showNodesDiscoveredWithin),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_allTime),
|
||||
leading: Radio<double>(value: 0),
|
||||
value: 0,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_lastHour),
|
||||
leading: Radio<double>(value: 1),
|
||||
value: 1,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_last6Hours),
|
||||
leading: Radio<double>(value: 6),
|
||||
value: 6,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_last24Hours),
|
||||
leading: Radio<double>(value: 24),
|
||||
value: 24,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_lastWeek),
|
||||
leading: Radio<double>(value: 168),
|
||||
value: 168,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -889,13 +1114,13 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
RadioListTile<UnitSystem>(
|
||||
title: Text(context.l10n.appSettings_unitsMetric),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
|
||||
value: UnitSystem.metric,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<UnitSystem>(
|
||||
title: Text(context.l10n.appSettings_unitsImperial),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
|
||||
value: UnitSystem.imperial,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -910,6 +1135,131 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showTranslationLanguageDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _TranslationLanguageDialogContent(
|
||||
currentLanguageCode:
|
||||
settingsService.settings.translationTargetLanguageCode,
|
||||
onLanguageSelected: (value) {
|
||||
settingsService.setTranslationTargetLanguageCode(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _downloadTranslationModel(
|
||||
BuildContext context,
|
||||
TranslationService translationService,
|
||||
AppSettingsService settingsService, {
|
||||
required String sourceUrl,
|
||||
String? fileName,
|
||||
String? id,
|
||||
}) async {
|
||||
if (sourceUrl.isEmpty) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.translation_enterUrlFirst),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await translationService.downloadModel(
|
||||
sourceUrl: sourceUrl,
|
||||
fileName: fileName,
|
||||
id: id,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.translation_modelDownloaded),
|
||||
);
|
||||
await settingsService.setTranslationEnabled(true);
|
||||
} on TranslationDownloadCancelled {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.translation_downloadStopped),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.translation_downloadFailed(error.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _translationLanguageLabel(BuildContext context, String? languageCode) {
|
||||
if (languageCode == null || languageCode.isEmpty) {
|
||||
return context.l10n.translation_useAppLanguage;
|
||||
}
|
||||
for (final option in supportedTranslationLanguages) {
|
||||
if (option.code == languageCode) {
|
||||
return option.label;
|
||||
}
|
||||
}
|
||||
return languageCode.toUpperCase();
|
||||
}
|
||||
|
||||
String _downloadProgressLabel(
|
||||
BuildContext context,
|
||||
TranslationService translationService,
|
||||
) {
|
||||
final fileName = translationService.downloadFileName ?? 'Model';
|
||||
if (fileName == 'Merging chunks...') {
|
||||
return context.l10n.translation_mergingChunks;
|
||||
}
|
||||
final currentMb = translationService.downloadedBytes / (1024 * 1024);
|
||||
final totalBytes = translationService.downloadTotalBytes;
|
||||
if (totalBytes == null || totalBytes <= 0) {
|
||||
return '$fileName: ${currentMb.toStringAsFixed(1)} MB';
|
||||
}
|
||||
final totalMb = totalBytes / (1024 * 1024);
|
||||
final percent = ((translationService.downloadProgress ?? 0) * 100)
|
||||
.toStringAsFixed(0);
|
||||
return '$fileName: ${currentMb.toStringAsFixed(1)} / ${totalMb.toStringAsFixed(1)} MB ($percent%)';
|
||||
}
|
||||
|
||||
Future<void> _deleteTranslationModel(
|
||||
BuildContext context,
|
||||
TranslationService translationService,
|
||||
TranslationModelRecord model,
|
||||
) async {
|
||||
try {
|
||||
await translationService.removeModel(model);
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.appSettings_translationModelDeleted(
|
||||
translationModelFriendlyName(model),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.appSettings_translationModelDeleteFailed('$error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _downloadedModelLabel(TranslationModelRecord model) {
|
||||
final sizeMb = model.fileSizeBytes / (1024 * 1024);
|
||||
final source = model.sourceUrl.isEmpty ? model.name : model.sourceUrl;
|
||||
return '${sizeMb.toStringAsFixed(1)} MB • $source';
|
||||
}
|
||||
|
||||
Widget _buildDebugCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
@@ -933,15 +1283,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
onChanged: (value) async {
|
||||
await settingsService.setAppDebugLogEnabled(value);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_appDebugLoggingEnabled
|
||||
: context.l10n.appSettings_appDebugLoggingDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_appDebugLoggingEnabled
|
||||
: context.l10n.appSettings_appDebugLoggingDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -950,3 +1299,179 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the [TextEditingController] for the manual model URL field so it
|
||||
/// survives rebuilds of the parent [Consumer3].
|
||||
class _TranslationUrlField extends StatefulWidget {
|
||||
const _TranslationUrlField({
|
||||
required this.initialValue,
|
||||
required this.onChanged,
|
||||
required this.onDownload,
|
||||
required this.downloadLabel,
|
||||
required this.isDownloading,
|
||||
required this.onCancel,
|
||||
required this.labelText,
|
||||
required this.stopLabel,
|
||||
});
|
||||
|
||||
final String initialValue;
|
||||
final ValueChanged<String> onChanged;
|
||||
final void Function(String url)? onDownload;
|
||||
final String downloadLabel;
|
||||
final bool isDownloading;
|
||||
final VoidCallback onCancel;
|
||||
final String labelText;
|
||||
final String stopLabel;
|
||||
|
||||
@override
|
||||
State<_TranslationUrlField> createState() => _TranslationUrlFieldState();
|
||||
}
|
||||
|
||||
class _TranslationUrlFieldState extends State<_TranslationUrlField> {
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: widget.onDownload == null
|
||||
? null
|
||||
: () => widget.onDownload!(_controller.text.trim()),
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text(widget.downloadLabel),
|
||||
),
|
||||
),
|
||||
if (widget.isDownloading) ...[
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: widget.onCancel,
|
||||
icon: const Icon(Icons.stop_circle_outlined),
|
||||
label: Text(widget.stopLabel),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog content for choosing the translation target language.
|
||||
/// Owns the search [TextEditingController] so it is properly disposed.
|
||||
class _TranslationLanguageDialogContent extends StatefulWidget {
|
||||
const _TranslationLanguageDialogContent({
|
||||
required this.currentLanguageCode,
|
||||
required this.onLanguageSelected,
|
||||
});
|
||||
|
||||
final String? currentLanguageCode;
|
||||
final ValueChanged<String?> onLanguageSelected;
|
||||
|
||||
@override
|
||||
State<_TranslationLanguageDialogContent> createState() =>
|
||||
_TranslationLanguageDialogContentState();
|
||||
}
|
||||
|
||||
class _TranslationLanguageDialogContentState
|
||||
extends State<_TranslationLanguageDialogContent> {
|
||||
late final TextEditingController _searchController;
|
||||
List<TranslationLanguageOption> _filtered = supportedTranslationLanguages;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.translation_targetLanguage),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
setState(() {
|
||||
_filtered = supportedTranslationLanguages.where((option) {
|
||||
return option.label.toLowerCase().contains(normalized) ||
|
||||
option.code.toLowerCase().contains(normalized);
|
||||
}).toList();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Flexible(
|
||||
child: RadioGroup<String?>(
|
||||
groupValue: widget.currentLanguageCode,
|
||||
onChanged: (value) {
|
||||
widget.onLanguageSelected(value);
|
||||
},
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
RadioListTile<String?>(
|
||||
value: null,
|
||||
title: Text(context.l10n.translation_useAppLanguage),
|
||||
),
|
||||
for (final option in _filtered)
|
||||
RadioListTile<String?>(
|
||||
value: option.code,
|
||||
title: Text(option.label),
|
||||
subtitle: Text(option.code.toUpperCase()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../l10n/l10n.dart';
|
||||
import '../services/ble_debug_log_service.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
enum _BleLogView { frames, rawLogRx }
|
||||
|
||||
@@ -52,10 +53,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.debugLog_bleCopied),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.debugLog_bleCopied),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
|
||||
@@ -4,36 +4,47 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/link_handler.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
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 '../widgets/byte_count_input.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../widgets/unread_divider.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
|
||||
class ChannelChatScreen extends StatefulWidget {
|
||||
final Channel channel;
|
||||
final int initialUnreadCount;
|
||||
|
||||
const ChannelChatScreen({super.key, required this.channel});
|
||||
const ChannelChatScreen({
|
||||
super.key,
|
||||
required this.channel,
|
||||
this.initialUnreadCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||
@@ -50,32 +61,46 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChannelSendAt;
|
||||
bool _channelSkipNextBottomSnap = false;
|
||||
String? _unreadDividerMessageId;
|
||||
|
||||
String? _cachedFormatLocale;
|
||||
late DateFormat _hmFormat;
|
||||
late DateFormat _mdFormat;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final idx = widget.channel.index;
|
||||
final unread = connector.getUnreadCountForChannelIndex(idx);
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
ChannelMessage? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(
|
||||
connector.getChannelMessages(widget.channel),
|
||||
unread,
|
||||
);
|
||||
if (unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(messages, unread);
|
||||
}
|
||||
setState(() {
|
||||
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
|
||||
});
|
||||
connector.setActiveChannel(idx);
|
||||
_connector = connector;
|
||||
if (anchor != null) {
|
||||
if (anchor != null && settings.jumpToOldestUnread) {
|
||||
_channelSkipNextBottomSnap = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
_scrollController.jumpToEstimatedOffset(
|
||||
unreadCount: unread,
|
||||
totalMessages: messages.length,
|
||||
onJumped: () {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -97,6 +122,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void _clearDividerAtBottom() {
|
||||
if (!_scrollController.showJumpToBottom.value &&
|
||||
_unreadDividerMessageId != null) {
|
||||
setState(() => _unreadDividerMessageId = null);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTextFieldFocusChange() {
|
||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||
_scrollController.handleKeyboardOpen();
|
||||
@@ -118,6 +150,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_connector?.setActiveChannel(null);
|
||||
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
|
||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||
_textFieldFocusNode.dispose();
|
||||
_textController.dispose();
|
||||
@@ -140,11 +173,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Future<void> _scrollToMessage(String messageId) async {
|
||||
final key = _messageKeys[messageId];
|
||||
if (key == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_originalMessageNotFound),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_originalMessageNotFound),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -317,6 +349,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
if (!_messageKeys.containsKey(message.messageId)) {
|
||||
_messageKeys[message.messageId] = GlobalKey();
|
||||
}
|
||||
final isUnreadAnchor =
|
||||
_unreadDividerMessageId != null &&
|
||||
message.messageId == _unreadDividerMessageId;
|
||||
return Container(
|
||||
key: _messageKeys[message.messageId]!,
|
||||
child: Builder(
|
||||
@@ -325,10 +360,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
return _buildMessageBubble(
|
||||
final bubble = _buildMessageBubble(
|
||||
message,
|
||||
textScale,
|
||||
);
|
||||
if (isUnreadAnchor) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const UnreadDivider(), bubble],
|
||||
);
|
||||
}
|
||||
return bubble;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -348,12 +390,32 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _markAsUnread(ChannelMessage message) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
var count = 0;
|
||||
var found = false;
|
||||
for (final m in messages) {
|
||||
if (m.messageId == message.messageId) found = true;
|
||||
if (found && !m.isOutgoing) count++;
|
||||
}
|
||||
connector.setChannelUnreadCount(widget.channel.index, count);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final gifId = _parseGifId(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
final gifId = GifHelper.parseGif(message.text);
|
||||
final poi = parseMarkerText(message.text);
|
||||
final translatedDisplayText =
|
||||
message.translatedText != null &&
|
||||
message.translatedText!.trim().isNotEmpty
|
||||
? message.translatedText!.trim()
|
||||
: message.text;
|
||||
final originalDisplayText = message.isOutgoing
|
||||
? message.originalText
|
||||
: (translatedDisplayText != message.text ? message.text : null);
|
||||
final displayPath = message.pathBytes.isNotEmpty
|
||||
? message.pathBytes
|
||||
: (message.pathVariants.isNotEmpty
|
||||
@@ -433,6 +495,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
poi,
|
||||
isOutgoing,
|
||||
textScale,
|
||||
message.senderName,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
@@ -504,12 +567,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: LinkHandler.buildLinkifyText(
|
||||
context: context,
|
||||
text: message.text,
|
||||
child: TranslatedMessageContent(
|
||||
displayText: translatedDisplayText,
|
||||
originalText: originalDisplayText,
|
||||
style: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
originalStyle: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
@@ -537,7 +606,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
'via ${_formatPathPrefixes(displayPath)}',
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(displayPath),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
@@ -558,7 +629,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
_formatTime(context, message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
@@ -682,8 +753,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
|
||||
|
||||
final gifId = _parseGifId(replyText);
|
||||
final poi = _parsePoiMessage(replyText);
|
||||
final gifId = GifHelper.parseGif(replyText);
|
||||
final poi = parseMarkerText(replyText);
|
||||
|
||||
Widget contentPreview;
|
||||
if (gifId != null) {
|
||||
@@ -794,30 +865,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String? _parseGifId(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
_PoiInfo? _parsePoiMessage(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(
|
||||
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|',
|
||||
).firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final lat = double.tryParse(match.group(1) ?? '');
|
||||
final lon = double.tryParse(match.group(2) ?? '');
|
||||
if (lat == null || lon == null) return null;
|
||||
final label = match.group(3) ?? '';
|
||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
||||
}
|
||||
|
||||
Widget _buildPoiMessage(
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
MarkerPayload poi,
|
||||
bool isOutgoing,
|
||||
double textScale, {
|
||||
double textScale,
|
||||
String senderName, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -837,12 +890,22 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
onPressed: () {
|
||||
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
|
||||
final fromName = isOutgoing ? selfName : senderName;
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: 'channel:${widget.channel.index}',
|
||||
label: poi.label,
|
||||
fromName: fromName,
|
||||
flags: poi.flags,
|
||||
isChannel: true,
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapScreen(
|
||||
highlightPosition: LatLng(poi.lat, poi.lon),
|
||||
highlightPosition: poi.position,
|
||||
highlightLabel: poi.label,
|
||||
highlightMarkerKey: key,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -880,7 +943,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
isScrollControlled: true,
|
||||
builder: (context) => GifPicker(
|
||||
onGifSelected: (gifId) {
|
||||
_textController.text = 'g:$gifId';
|
||||
_textController.text = GifHelper.encodeGif(gifId);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -994,6 +1057,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Widget _buildMessageComposer() {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -1025,11 +1089,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, child) {
|
||||
final gifId = _parseGifId(value.text);
|
||||
final gifId = GifHelper.parseGif(value.text);
|
||||
if (gifId != null) {
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
@@ -1074,27 +1144,33 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
return ByteCountedTextField(
|
||||
maxBytes: maxBytes,
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
encoder:
|
||||
connector.isChannelSmazEnabled(widget.channel.index)
|
||||
? (text) => connector.prepareChannelOutboundText(
|
||||
widget.channel.index,
|
||||
text,
|
||||
)
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
horizontal: 20,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -1102,6 +1178,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
tooltip: context.l10n.chat_sendMessage,
|
||||
onPressed: _sendMessage,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
@@ -1112,49 +1189,108 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage() {
|
||||
Future<void> _showTranslationOptions() async {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final settings = settingsService.settings;
|
||||
await showMessageTranslationSheet(
|
||||
context: context,
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
selectedLanguageCode: settings.translationTargetLanguageCode,
|
||||
onEnabledChanged: settingsService.setComposerTranslationEnabled,
|
||||
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
final text = _textController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
if (_lastChannelSendAt != null &&
|
||||
now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
|
||||
content: Text(context.l10n.chat_sendCooldown),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_lastChannelSendAt = now;
|
||||
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final translationService = context.read<TranslationService>();
|
||||
|
||||
String messageText = text;
|
||||
String? originalText;
|
||||
String? translatedLanguageCode;
|
||||
String? translationModelId;
|
||||
if (settings.translationEnabled) {
|
||||
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
if (translationService.shouldTranslateOutgoing(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
)) {
|
||||
final result = await translationService.translateOutgoingText(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null &&
|
||||
result.status == MessageTranslationStatus.completed &&
|
||||
result.translatedText.isNotEmpty) {
|
||||
messageText = result.translatedText;
|
||||
originalText = text;
|
||||
translatedLanguageCode = result.targetLanguageCode;
|
||||
translationModelId = result.modelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_replyingToMessage != null) {
|
||||
messageText = '@[${_replyingToMessage!.senderName}] $text';
|
||||
messageText = '@[${_replyingToMessage!.senderName}] $messageText';
|
||||
}
|
||||
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
if (utf8.encode(messageText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
final outboundText = connector.prepareChannelOutboundText(
|
||||
widget.channel.index,
|
||||
messageText,
|
||||
);
|
||||
if (utf8.encode(outboundText).length > maxBytes) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.sendChannelMessage(widget.channel, messageText);
|
||||
_textController.clear();
|
||||
_cancelReply();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
connector.sendChannelMessage(
|
||||
widget.channel,
|
||||
messageText,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
String _formatTime(BuildContext context, DateTime time) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(time);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
if (locale != _cachedFormatLocale) {
|
||||
_cachedFormatLocale = locale;
|
||||
_hmFormat = DateFormat.Hm(locale);
|
||||
_mdFormat = DateFormat.Md(locale);
|
||||
}
|
||||
final hm = _hmFormat.format(time);
|
||||
|
||||
if (diff.inDays > 0) {
|
||||
return '${time.day}/${time.month} ${time.hour}:${time.minute.toString().padLeft(2, '0')}';
|
||||
return '${_mdFormat.format(time)} $hm';
|
||||
} else {
|
||||
return '${time.hour}:${time.minute.toString().padLeft(2, '0')}';
|
||||
return hm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,6 +1346,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_copyMessageText(message.text);
|
||||
},
|
||||
),
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
title: Text(context.l10n.chat_markAsUnread),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_markAsUnread(message);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: Text(context.l10n.common_delete),
|
||||
@@ -1251,23 +1396,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
message.senderName,
|
||||
message.text,
|
||||
);
|
||||
final reactionText = 'r:$hash:$emojiIndex';
|
||||
final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
|
||||
connector.sendChannelMessage(widget.channel, reactionText);
|
||||
}
|
||||
|
||||
void _copyMessageText(String text) {
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied)));
|
||||
content: Text(context.l10n.chat_messageCopied),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteMessage(ChannelMessage message) async {
|
||||
await context.read<MeshCoreConnector>().deleteChannelMessage(message);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted)));
|
||||
content: Text(context.l10n.chat_messageDeleted),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
@@ -1427,11 +1574,3 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PoiInfo {
|
||||
final double lat;
|
||||
final double lon;
|
||||
final String label;
|
||||
|
||||
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -304,6 +304,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||
class _ChannelMessagePathMapScreenState
|
||||
extends State<ChannelMessagePathMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
Uint8List? _selectedPath;
|
||||
@@ -330,6 +332,18 @@ class _ChannelMessagePathMapScreenState
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
double _getPathDistance(List<LatLng> points) {
|
||||
double totalDistance = 0.0;
|
||||
final distanceCalculator = Distance();
|
||||
@@ -357,6 +371,70 @@ class _ChannelMessagePathMapScreenState
|
||||
});
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(initialCenter, initialZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: 16,
|
||||
top: 16,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
@@ -372,6 +450,7 @@ class _ChannelMessagePathMapScreenState
|
||||
primaryPath,
|
||||
widget.message.pathVariants,
|
||||
);
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final selectedPathTmp = _resolveSelectedPath(
|
||||
_selectedPath,
|
||||
observedPaths,
|
||||
@@ -451,10 +530,20 @@ class _ChannelMessagePathMapScreenState
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
@@ -486,6 +575,12 @@ class _ChannelMessagePathMapScreenState
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
if (observedPaths.length > 1)
|
||||
_buildPathSelector(context, observedPaths, selectedIndex, (
|
||||
index,
|
||||
@@ -822,7 +917,8 @@ List<_PathHop> _buildPathHops(
|
||||
) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
final candidatesByPrefix = <int, List<Contact>>{};
|
||||
for (final contact in connector.allContacts) {
|
||||
final allContacts = connector.allContacts;
|
||||
for (final contact in allContacts) {
|
||||
if (contact.publicKey.isEmpty) continue;
|
||||
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||
continue;
|
||||
@@ -839,7 +935,8 @@ List<_PathHop> _buildPathHops(
|
||||
: null;
|
||||
var previousPosition = startPoint;
|
||||
final distance = Distance();
|
||||
|
||||
var lastDistance = 0.0;
|
||||
var bestDistance = 0.0;
|
||||
final hops = <_PathHop>[];
|
||||
for (var i = 0; i < pathBytes.length; i++) {
|
||||
final searchPoint = i == 0 ? startPoint : previousPosition;
|
||||
@@ -848,7 +945,7 @@ List<_PathHop> _buildPathHops(
|
||||
if (candidates != null && candidates.isNotEmpty) {
|
||||
var bestIndex = 0;
|
||||
if (searchPoint != null) {
|
||||
var bestDistance = double.infinity;
|
||||
bestDistance = double.infinity;
|
||||
for (var j = 0; j < candidates.length; j++) {
|
||||
final candidate = candidates[j];
|
||||
if (!candidate.hasLocation ||
|
||||
@@ -876,6 +973,16 @@ List<_PathHop> _buildPathHops(
|
||||
if (resolvedPosition != null) {
|
||||
previousPosition = resolvedPosition;
|
||||
}
|
||||
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
|
||||
if (lastDistance + bestDistance > 50000 &&
|
||||
candidates != null &&
|
||||
candidates.isNotEmpty) {
|
||||
i--;
|
||||
lastDistance = bestDistance;
|
||||
continue;
|
||||
}
|
||||
lastDistance = bestDistance;
|
||||
|
||||
hops.add(
|
||||
_PathHop(
|
||||
index: i + 1,
|
||||
|
||||
@@ -24,6 +24,7 @@ import '../widgets/empty_state.dart';
|
||||
import '../widgets/qr_code_display.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channel_chat_screen.dart';
|
||||
import 'community_qr_scanner_screen.dart';
|
||||
import 'contacts_screen.dart';
|
||||
@@ -127,7 +128,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(context.l10n.channels_title, indicators: false),
|
||||
title: AppBarTitle(context.l10n.channels_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
@@ -491,13 +492,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
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),
|
||||
builder: (context) => ChannelChatScreen(
|
||||
channel: channel,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -809,15 +816,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -837,13 +841,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
nextIndex,
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
name,
|
||||
),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -897,15 +898,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
final name = nameController.text.trim();
|
||||
final pskHex = pskController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -914,15 +912,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
try {
|
||||
psk = Channel.parsePskHex(pskHex);
|
||||
} on FormatException {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_pskMustBe32Hex,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_pskMustBe32Hex,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -930,13 +925,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, name, psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
name,
|
||||
),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -967,11 +959,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, 'Public', psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_publicChannelAdded,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_publicChannelAdded,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1097,15 +1088,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
onPressed: () async {
|
||||
var hashtag = hashtagController.text.trim();
|
||||
if (hashtag.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1125,15 +1113,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
} else {
|
||||
// Community hashtag - HMAC derivation from community secret
|
||||
if (selectedCommunity == null) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.community_selectCommunity,
|
||||
),
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.community_selectCommunity,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1159,12 +1144,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
psk,
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
channelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
channelName,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1259,13 +1243,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext.l10n.community_enterName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext.l10n.community_enterName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1301,11 +1282,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
_loadCommunities();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.community_created(name),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.community_created(name),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1494,10 +1474,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
try {
|
||||
psk = Channel.parsePskHex(pskHex);
|
||||
} on FormatException {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
dialogContext,
|
||||
content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1510,16 +1489,18 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
smazEnabled,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||
);
|
||||
} catch (e, st) {
|
||||
debugPrint(st.toString());
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update channel: $e')),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelUpdateFailed('$e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -1559,21 +1540,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleted(channel.name),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleted(channel.name),
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleteFailed(channel.name),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleteFailed(channel.name),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1594,8 +1573,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
|
||||
final psk = Channel.parsePskHex(Channel.publicChannelPsk);
|
||||
connector.setChannel(0, 'Public', psk);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.channels_publicChannelAdded)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.channels_publicChannelAdded),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1810,12 +1790,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
_loadCommunities();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.community_deleted(community.name),
|
||||
),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_deleted(community.name)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+263
-126
@@ -9,25 +9,27 @@ import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../helpers/link_handler.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/elements_ui.dart';
|
||||
import '../widgets/byte_count_input.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
@@ -35,16 +37,25 @@ import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/path_selection_dialog.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../widgets/unread_divider.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
final int initialUnreadCount;
|
||||
|
||||
const ChatScreen({super.key, required this.contact});
|
||||
const ChatScreen({
|
||||
super.key,
|
||||
required this.contact,
|
||||
this.initialUnreadCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
@@ -58,6 +69,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
bool _isLoadingOlder = false;
|
||||
MeshCoreConnector? _connector;
|
||||
Message? _pendingUnreadScrollTarget;
|
||||
String? _unreadDividerMessageId;
|
||||
DateTime? _lastTextSendAt;
|
||||
|
||||
@override
|
||||
@@ -65,34 +77,47 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
super.initState();
|
||||
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final keyHex = widget.contact.publicKeyHex;
|
||||
final unread = connector.getUnreadCountForContactKey(keyHex);
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getMessages(widget.contact);
|
||||
Message? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadAnchor(
|
||||
connector.getMessages(widget.contact),
|
||||
unread,
|
||||
);
|
||||
if (unread > 0) {
|
||||
anchor = _findOldestUnreadAnchor(messages, unread);
|
||||
}
|
||||
setState(() {
|
||||
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
|
||||
if (anchor != null && settings.jumpToOldestUnread) {
|
||||
_pendingUnreadScrollTarget = anchor;
|
||||
}
|
||||
});
|
||||
connector.setActiveContact(keyHex);
|
||||
_connector = connector;
|
||||
if (anchor != null) {
|
||||
setState(() => _pendingUnreadScrollTarget = anchor);
|
||||
if (anchor != null && settings.jumpToOldestUnread) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final ctx = _unreadScrollKey.currentContext;
|
||||
if (ctx != null) {
|
||||
Scrollable.ensureVisible(
|
||||
ctx,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
alignment: 0.15,
|
||||
);
|
||||
}
|
||||
setState(() => _pendingUnreadScrollTarget = null);
|
||||
_scrollController.jumpToEstimatedOffset(
|
||||
unreadCount: unread,
|
||||
totalMessages: messages.length,
|
||||
onJumped: () async {
|
||||
if (!mounted) return;
|
||||
final ctx = _unreadScrollKey.currentContext;
|
||||
if (ctx != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
ctx,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
alignment: 0.15,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _pendingUnreadScrollTarget = null);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -111,6 +136,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void _clearDividerAtBottom() {
|
||||
if (!_scrollController.showJumpToBottom.value &&
|
||||
_unreadDividerMessageId != null) {
|
||||
setState(() => _unreadDividerMessageId = null);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTextFieldFocusChange() {
|
||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||
_scrollController.handleKeyboardOpen();
|
||||
@@ -132,6 +164,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_connector?.setActiveContact(null);
|
||||
_scrollController.showJumpToBottom.removeListener(_clearDividerAtBottom);
|
||||
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
|
||||
_textFieldFocusNode.dispose();
|
||||
_textController.dispose();
|
||||
@@ -290,6 +323,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
tooltip: context.l10n.chat_pathManagement,
|
||||
onPressed: () => _showPathHistory(context),
|
||||
),
|
||||
const RadioStatsIconButton(),
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
return PopupMenuButton<String>(
|
||||
@@ -362,7 +396,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
const RadioStatsIconButton(),
|
||||
],
|
||||
),
|
||||
body: Consumer<MeshCoreConnector>(
|
||||
@@ -474,6 +507,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
senderName: resolvedContact.type == advTypeRoom
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
: contact.name,
|
||||
sourceId: widget.contact.publicKeyHex,
|
||||
isRoomServer: resolvedContact.type == advTypeRoom,
|
||||
textScale: textScale,
|
||||
onTap: () => _openMessagePath(message, contact),
|
||||
@@ -481,10 +515,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
onRetryReaction: (msg, emoji) =>
|
||||
_sendReaction(msg, contact, emoji),
|
||||
);
|
||||
final isUnreadAnchor =
|
||||
_unreadDividerMessageId != null &&
|
||||
message.messageId == _unreadDividerMessageId;
|
||||
final child = isUnreadAnchor
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const UnreadDivider(), bubble],
|
||||
)
|
||||
: bubble;
|
||||
if (identical(message, _pendingUnreadScrollTarget)) {
|
||||
return KeyedSubtree(key: _unreadScrollKey, child: bubble);
|
||||
return KeyedSubtree(key: _unreadScrollKey, child: child);
|
||||
}
|
||||
return bubble;
|
||||
return child;
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -492,9 +535,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _markAsUnread(Message message) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final messages = connector.getMessages(widget.contact);
|
||||
var count = 0;
|
||||
var found = false;
|
||||
for (final m in messages) {
|
||||
if (m.messageId == message.messageId) found = true;
|
||||
if (found && !m.isOutgoing && !m.isCli) count++;
|
||||
}
|
||||
connector.setContactUnreadCount(widget.contact.publicKeyHex, count);
|
||||
}
|
||||
|
||||
Widget _buildInputBar(MeshCoreConnector connector) {
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
@@ -509,11 +565,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, child) {
|
||||
final gifId = _parseGifId(value.text);
|
||||
final gifId = GifHelper.parseGif(value.text);
|
||||
if (gifId != null) {
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
@@ -555,24 +617,35 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
return ByteCountedTextField(
|
||||
maxBytes: maxBytes,
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
inputFormatters: [
|
||||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||||
],
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
onSubmitted: (_) => _sendMessage(connector),
|
||||
encoder:
|
||||
connector.isContactSmazEnabled(
|
||||
widget.contact.publicKeyHex,
|
||||
)
|
||||
? (text) => connector.prepareContactOutboundText(
|
||||
widget.contact,
|
||||
text,
|
||||
)
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: const OutlineInputBorder(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(connector),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -580,6 +653,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filled(
|
||||
icon: const Icon(Icons.send),
|
||||
tooltip: context.l10n.chat_sendMessageTo(
|
||||
_resolveContact(connector).name,
|
||||
),
|
||||
onPressed: () => _sendMessage(connector),
|
||||
),
|
||||
],
|
||||
@@ -588,49 +664,96 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String? _parseGifId(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
void _showGifPicker(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => GifPicker(
|
||||
onGifSelected: (gifId) {
|
||||
_textController.text = 'g:$gifId';
|
||||
_textController.text = GifHelper.encodeGif(gifId);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(MeshCoreConnector connector) {
|
||||
Future<void> _showTranslationOptions() async {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final settings = settingsService.settings;
|
||||
await showMessageTranslationSheet(
|
||||
context: context,
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
selectedLanguageCode: settings.translationTargetLanguageCode,
|
||||
onEnabledChanged: settingsService.setComposerTranslationEnabled,
|
||||
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sendMessage(MeshCoreConnector connector) async {
|
||||
final text = _textController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
if (_lastTextSendAt != null &&
|
||||
now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
|
||||
content: Text(context.l10n.chat_sendCooldown),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_lastTextSendAt = now;
|
||||
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final translationService = context.read<TranslationService>();
|
||||
var outgoingText = text;
|
||||
String? originalText;
|
||||
String? translatedLanguageCode;
|
||||
String? translationModelId;
|
||||
if (settings.translationEnabled) {
|
||||
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
if (translationService.shouldTranslateOutgoing(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
)) {
|
||||
final result = await translationService.translateOutgoingText(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null &&
|
||||
result.status == MessageTranslationStatus.completed &&
|
||||
result.translatedText.isNotEmpty) {
|
||||
outgoingText = result.translatedText;
|
||||
originalText = text;
|
||||
translatedLanguageCode = result.targetLanguageCode;
|
||||
translationModelId = result.modelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
if (utf8.encode(text).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
final outboundText = connector.prepareContactOutboundText(
|
||||
_resolveContact(connector),
|
||||
outgoingText,
|
||||
);
|
||||
if (utf8.encode(outboundText).length > maxBytes) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.sendMessage(_resolveContact(connector), text);
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
connector.sendMessage(
|
||||
_resolveContact(connector),
|
||||
outgoingText,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
);
|
||||
}
|
||||
|
||||
void _showPathHistory(BuildContext context) {
|
||||
@@ -805,15 +928,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_showFullPathDialog(context, path.pathBytes),
|
||||
onTap: () async {
|
||||
if (path.pathBytes.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context
|
||||
.l10n
|
||||
.chat_pathDetailsNotAvailable,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.chat_pathDetailsNotAvailable,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -897,11 +1017,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_resolveContact(connector),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_pathCleared),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_pathCleared),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
@@ -927,11 +1046,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
pathLen: -1,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_floodModeEnabled),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_floodModeEnabled),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
@@ -965,11 +1083,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
|
||||
if (pathBytes.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_pathDetailsNotAvailable),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_pathDetailsNotAvailable),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1082,11 +1199,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
: (verified
|
||||
? context.l10n.chat_pathDeviceConfirmed
|
||||
: context.l10n.chat_pathDeviceNotConfirmed);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_pathSetHops(hopCount, status)),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_pathSetHops(hopCount, status)),
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1102,8 +1218,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
_formatContactLastMessage(contact.lastMessageAt),
|
||||
@@ -1254,11 +1376,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
void _openChat(BuildContext context, Contact contact) {
|
||||
// Check if this is a repeater
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
||||
connector.markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ChatScreen(contact: contact, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1395,6 +1521,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_copyMessageText(message.text);
|
||||
},
|
||||
),
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
title: Text(context.l10n.chat_markAsUnread),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_markAsUnread(message);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: Text(context.l10n.common_delete),
|
||||
@@ -1435,26 +1570,29 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
void _copyMessageText(String text) {
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied)));
|
||||
content: Text(context.l10n.chat_messageCopied),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteMessage(Message message) async {
|
||||
await context.read<MeshCoreConnector>().deleteMessage(message);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted)));
|
||||
content: Text(context.l10n.chat_messageDeleted),
|
||||
);
|
||||
}
|
||||
|
||||
void _retryMessage(Message message) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
// Retry using the contact's current path override setting
|
||||
connector.sendMessage(_resolveContact(connector), message.text);
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
||||
content: Text(context.l10n.chat_retryingMessage),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEmojiPicker(Message message, Contact senderContact) {
|
||||
@@ -1486,7 +1624,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
senderName,
|
||||
message.text,
|
||||
);
|
||||
final reactionText = 'r:$hash:$emojiIndex';
|
||||
final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
|
||||
connector.sendMessage(_resolveContact(connector), reactionText);
|
||||
}
|
||||
}
|
||||
@@ -1499,10 +1637,12 @@ class _MessageBubble extends StatelessWidget {
|
||||
final VoidCallback? onLongPress;
|
||||
final void Function(Message message, String emoji)? onRetryReaction;
|
||||
final double textScale;
|
||||
final String sourceId;
|
||||
|
||||
const _MessageBubble({
|
||||
required this.message,
|
||||
required this.senderName,
|
||||
required this.sourceId,
|
||||
required this.isRoomServer,
|
||||
required this.textScale,
|
||||
this.onTap,
|
||||
@@ -1516,8 +1656,8 @@ class _MessageBubble extends StatelessWidget {
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final gifId = _parseGifId(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
final gifId = GifHelper.parseGif(message.text);
|
||||
final poi = parseMarkerText(message.text);
|
||||
final isFailed = message.status == MessageStatus.failed;
|
||||
final bubbleColor = isFailed
|
||||
? colorScheme.errorContainer
|
||||
@@ -1533,6 +1673,14 @@ class _MessageBubble extends StatelessWidget {
|
||||
if (isRoomServer && !isOutgoing) {
|
||||
messageText = message.text.substring(4.clamp(0, message.text.length));
|
||||
}
|
||||
final translatedDisplayText =
|
||||
message.translatedText != null &&
|
||||
message.translatedText!.trim().isNotEmpty
|
||||
? message.translatedText!.trim()
|
||||
: messageText;
|
||||
final originalDisplayText = isOutgoing
|
||||
? message.originalText
|
||||
: (translatedDisplayText != messageText ? messageText : null);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
@@ -1601,6 +1749,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
textColor,
|
||||
metaColor,
|
||||
textScale,
|
||||
senderName,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
@@ -1662,13 +1811,17 @@ class _MessageBubble extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: LinkHandler.buildLinkifyText(
|
||||
context: context,
|
||||
text: messageText,
|
||||
child: TranslatedMessageContent(
|
||||
displayText: translatedDisplayText,
|
||||
originalText: originalDisplayText,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
originalStyle: TextStyle(
|
||||
color: textColor.withValues(alpha: 0.78),
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
@@ -1778,31 +1931,13 @@ class _MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String? _parseGifId(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
_PoiInfo? _parsePoiMessage(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(
|
||||
r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$',
|
||||
).firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final lat = double.tryParse(match.group(1) ?? '');
|
||||
final lon = double.tryParse(match.group(2) ?? '');
|
||||
if (lat == null || lon == null) return null;
|
||||
final label = match.group(3) ?? '';
|
||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
||||
}
|
||||
|
||||
Widget _buildPoiMessage(
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
MarkerPayload poi,
|
||||
Color textColor,
|
||||
Color metaColor,
|
||||
double textScale, {
|
||||
double textScale,
|
||||
String senderName, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
return Row(
|
||||
@@ -1812,13 +1947,23 @@ class _MessageBubble extends StatelessWidget {
|
||||
icon: Icon(Icons.location_on_outlined, color: textColor),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
final selfName = context.read<MeshCoreConnector>().selfName ?? 'Me';
|
||||
final fromName = message.isOutgoing ? selfName : senderName;
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: sourceId,
|
||||
label: poi.label,
|
||||
fromName: fromName,
|
||||
flags: poi.flags,
|
||||
isChannel: false,
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapScreen(
|
||||
highlightPosition: LatLng(poi.lat, poi.lon),
|
||||
highlightPosition: poi.position,
|
||||
highlightLabel: poi.label,
|
||||
highlightMarkerKey: key,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1999,11 +2144,3 @@ class _MessageBubble extends StatelessWidget {
|
||||
return '$hour:$minute';
|
||||
}
|
||||
}
|
||||
|
||||
class _PoiInfo {
|
||||
final double lat;
|
||||
final double lon;
|
||||
final String label;
|
||||
|
||||
const _PoiInfo({required this.lat, required this.lon, required this.label});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/qr_scanner_widget.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
/// Screen for scanning community QR codes to join communities.
|
||||
///
|
||||
@@ -76,11 +77,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -93,12 +93,11 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
|
||||
void _showInvalidQrError(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -229,11 +228,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_joined(community.name)),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_joined(community.name)),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
|
||||
// Return to previous screen
|
||||
|
||||
@@ -24,6 +24,7 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
|
||||
final c = context.read<MeshCoreConnector>();
|
||||
_connector = c;
|
||||
c.acquireRadioStatsPolling();
|
||||
c.setPollingInterval(1);
|
||||
c.radioStatsNotifier.addListener(_onStatsUpdate);
|
||||
}
|
||||
|
||||
@@ -44,6 +45,7 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
|
||||
void dispose() {
|
||||
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
|
||||
_connector?.releaseRadioStatsPolling();
|
||||
_connector?.setPollingInterval(30);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../models/contact_group.dart';
|
||||
import '../services/ui_view_state_service.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
@@ -27,6 +28,7 @@ import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/repeater_login_dialog.dart';
|
||||
import '../widgets/room_login_dialog.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'discovery_screen.dart';
|
||||
@@ -150,9 +152,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
}
|
||||
|
||||
void _showGroupsUnavailableMessage(BuildContext context) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.common_loading)));
|
||||
content: Text(context.l10n.common_loading),
|
||||
);
|
||||
}
|
||||
|
||||
void _setupFrameListener() {
|
||||
@@ -169,10 +172,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
// Validate packet has expected minimum size (98+ bytes per protocol)
|
||||
if (advertPacket.length < 98) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
);
|
||||
}
|
||||
_pendingOperations.remove(ContactOperationType.export);
|
||||
@@ -187,24 +189,23 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactImported),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,25 +217,22 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactImportFailed),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactImportFailed),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
);
|
||||
}
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -271,8 +269,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
final clipboardData = await Clipboard.getData('text/plain');
|
||||
if (clipboardData == null || clipboardData.text == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_clipboardEmpty),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -280,8 +279,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
final text = clipboardData.text!.trim();
|
||||
if (!text.startsWith('meshcore://')) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -294,8 +294,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
connector.importContact(importContactFrame);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -330,10 +331,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
),
|
||||
onTap: () => {
|
||||
connector.sendSelfAdvert(flood: false),
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -347,10 +347,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
),
|
||||
onTap: () => {
|
||||
connector.sendSelfAdvert(flood: true),
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -394,7 +393,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
children: [
|
||||
const Icon(Icons.person_add_rounded),
|
||||
const SizedBox(width: 8),
|
||||
Text("Discovered Contacts"),
|
||||
Text(context.l10n.discoveredContacts_Title),
|
||||
],
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
@@ -932,10 +931,17 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
} else if (contact.type == advTypeRoom) {
|
||||
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
||||
} else {
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ChatScreen(contact: contact, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -963,13 +969,16 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
context: context,
|
||||
builder: (context) => RepeaterLoginDialog(
|
||||
repeater: repeater,
|
||||
onLogin: (password) {
|
||||
onLogin: (password, isAdmin) {
|
||||
// Navigate to repeater hub screen after successful login
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
RepeaterHubScreen(repeater: repeater, password: password),
|
||||
builder: (context) => RepeaterHubScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
isAdmin: isAdmin,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -986,15 +995,23 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
context: context,
|
||||
builder: (context) => RoomLoginDialog(
|
||||
room: room,
|
||||
onLogin: (password) {
|
||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
||||
onLogin: (password, isAdmin) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
room.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
destination == RoomLoginDestination.management
|
||||
? RepeaterHubScreen(repeater: room, password: password)
|
||||
: ChatScreen(contact: room),
|
||||
? RepeaterHubScreen(
|
||||
repeater: room,
|
||||
password: password,
|
||||
isAdmin: isAdmin,
|
||||
)
|
||||
: ChatScreen(contact: room, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1117,7 +1134,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return CheckboxListTile(
|
||||
value: isSelected,
|
||||
title: Text(contact.name),
|
||||
subtitle: Text(contact.typeLabel),
|
||||
subtitle: Text(
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setDialogState(() {
|
||||
if (value == true) {
|
||||
@@ -1146,19 +1165,17 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_groupNameRequired),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_groupNameRequired),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (name.toLowerCase() ==
|
||||
contactsAllGroupsValue.toLowerCase()) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_groupNameReserved),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_groupNameReserved),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1167,11 +1184,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return g.name.toLowerCase() == name.toLowerCase();
|
||||
});
|
||||
if (exists) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.contacts_groupAlreadyExists(name),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.contacts_groupAlreadyExists(name),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1240,9 +1256,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
if (isRepeater) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: contact.pathBytesForDisplay.isNotEmpty
|
||||
? Text(context.l10n.contacts_pathTrace)
|
||||
: Text(context.l10n.contacts_ping),
|
||||
title: Text(context.l10n.contacts_ping),
|
||||
onTap: () {
|
||||
final hw = context
|
||||
.read<MeshCoreConnector>()
|
||||
@@ -1251,11 +1265,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: contact.pathBytesForDisplay.isNotEmpty
|
||||
? context.l10n.contacts_repeaterPathTrace
|
||||
: context.l10n.contacts_repeaterPing,
|
||||
path: contact.pathBytesForDisplay,
|
||||
flipPathAround: true,
|
||||
title: context.l10n.contacts_repeaterPing,
|
||||
path: Uint8List.fromList([contact.publicKey.first]),
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
@@ -1274,9 +1285,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
] else if (isRoom) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: contact.pathLength > 0
|
||||
? Text(context.l10n.contacts_pathTrace)
|
||||
: Text(context.l10n.contacts_ping),
|
||||
title: Text(context.l10n.contacts_pathTrace),
|
||||
onTap: () {
|
||||
final hw = context
|
||||
.read<MeshCoreConnector>()
|
||||
@@ -1288,7 +1297,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
title: contact.pathBytesForDisplay.isNotEmpty
|
||||
? context.l10n.contacts_roomPathTrace
|
||||
: context.l10n.contacts_roomPing,
|
||||
path: contact.pathBytesForDisplay,
|
||||
path: contact.pathBytesForDisplay.isNotEmpty
|
||||
? contact.pathBytesForDisplay
|
||||
: Uint8List.fromList([contact.publicKey.first]),
|
||||
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
@@ -1467,7 +1478,7 @@ class _ContactTile extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
contact.pathLabel,
|
||||
contact.pathLabel(context.l10n),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -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/snack_bar_builder.dart';
|
||||
|
||||
enum DiscoverySortOption { lastSeen, name, type }
|
||||
|
||||
@@ -38,6 +39,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
DateTime _resolveLastSeen(Contact contact) {
|
||||
if (contact.type != advTypeChat) return contact.lastSeen;
|
||||
return contact.lastMessageAt.isAfter(contact.lastSeen)
|
||||
? contact.lastMessageAt
|
||||
: contact.lastSeen;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
@@ -108,11 +116,56 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text(
|
||||
_formatLastSeen(context, contact.lastSeen),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
// Clamp text scaling in trailing section to prevent overflow while
|
||||
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||
trailing: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(
|
||||
MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(1.0).clamp(1.0, 1.3),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatLastSeen(
|
||||
context,
|
||||
_resolveLastSeen(contact),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (contact.hasLocation)
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
if (contact.rawPacket != null)
|
||||
const SizedBox(width: 2),
|
||||
if (contact.rawPacket != null)
|
||||
Icon(
|
||||
Icons.cell_tower,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
@@ -182,8 +235,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
final hexString = pubKeyToHex(contact.rawPacket!);
|
||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
);
|
||||
break;
|
||||
case 'delete_contact':
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -56,12 +57,16 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
static const double _maxAntennaFeet = 400.0;
|
||||
static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet;
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final LineOfSightService _lineOfSightService = LineOfSightService();
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
LineOfSightPathResult? _result;
|
||||
LineOfSightObstruction? _selectedObstruction;
|
||||
LineOfSightEndpoint? _start;
|
||||
LineOfSightEndpoint? _end;
|
||||
final List<LineOfSightEndpoint> _customEndpoints = [];
|
||||
@@ -98,10 +103,85 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
_lineOfSightService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(initialCenter, initialZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls({
|
||||
required LatLng initialCenter,
|
||||
required double initialZoom,
|
||||
required LatLngBounds? bounds,
|
||||
}) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final topOffset = _showHud
|
||||
? math.min(screenHeight * 0.52 + 24, screenHeight - 220)
|
||||
: 12.0;
|
||||
return Positioned(
|
||||
top: topOffset,
|
||||
left: 12,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _resetMapView(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runLos() async {
|
||||
final start = _start;
|
||||
final end = _end;
|
||||
@@ -111,6 +191,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
if (start == null || end == null) {
|
||||
setState(() {
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = _errorSelectStartEnd;
|
||||
});
|
||||
return;
|
||||
@@ -142,6 +223,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
}
|
||||
setState(() {
|
||||
_result = result;
|
||||
_selectedObstruction = _defaultObstructionFor(result);
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -156,6 +238,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
}
|
||||
setState(() {
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = context.l10n.losRunFailed(e.toString());
|
||||
});
|
||||
} finally {
|
||||
@@ -184,6 +267,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
void _selectFromMap(LineOfSightEndpoint endpoint) {
|
||||
setState(() {
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = null;
|
||||
if (_start == null || (_start != null && _end != null)) {
|
||||
_start = endpoint;
|
||||
@@ -241,6 +325,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
_start = null;
|
||||
_end = null;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
_error = _errorSelectStartEnd;
|
||||
});
|
||||
}
|
||||
@@ -251,6 +336,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
if (identical(_start, endpoint)) _start = null;
|
||||
if (identical(_end, endpoint)) _end = null;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -318,6 +404,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
? LatLngBounds.fromPoints(mapPoints)
|
||||
: null;
|
||||
final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
if (!_didReceivePositionUpdate) {
|
||||
_showMarkerLabels = initialZoom >= _labelZoomThreshold;
|
||||
}
|
||||
@@ -343,6 +430,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
body: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
@@ -355,7 +443,19 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onLongPress: (_, point) => _addCustomPoint(point),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
@@ -377,9 +477,17 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
),
|
||||
if (_result != null && _result!.segments.isNotEmpty)
|
||||
PolylineLayer(polylines: _buildSegmentPolylines(_result!)),
|
||||
MarkerLayer(markers: _buildMarkers(endpoints)),
|
||||
MarkerLayer(
|
||||
markers: _buildMarkers(endpoints, _primaryObstructions()),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
bounds: bounds,
|
||||
),
|
||||
if (_showHud)
|
||||
Positioned(
|
||||
left: 12,
|
||||
@@ -445,6 +553,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
);
|
||||
final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz;
|
||||
final kFactorUsed = segment?.usedKFactor;
|
||||
final obstructions =
|
||||
segment?.obstructions ?? const <LineOfSightObstruction>[];
|
||||
final endpoints = _visibleEndpoints();
|
||||
final distanceUnit = isImperial ? 'mi' : 'km';
|
||||
final heightUnit = isImperial ? 'ft' : 'm';
|
||||
@@ -463,31 +573,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (segment != null)
|
||||
SizedBox(
|
||||
height: 160,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _LosProfilePainter(
|
||||
samples: segment.samples,
|
||||
distanceUnit: distanceUnit,
|
||||
heightUnit: heightUnit,
|
||||
badgeTextStyle:
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
) ??
|
||||
const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
terrainLabel: context.l10n.losLegendTerrain,
|
||||
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||
),
|
||||
),
|
||||
)
|
||||
_buildProfileView(segment, distanceUnit, heightUnit, isImperial)
|
||||
else
|
||||
SizedBox(
|
||||
height: 44,
|
||||
@@ -519,6 +605,96 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (obstructions.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.losBlockedSpotsTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.losBlockedSpotsHint,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final obstruction in obstructions)
|
||||
ChoiceChip(
|
||||
label: Text(
|
||||
_obstructionChipLabel(obstruction, isImperial),
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
selected:
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex,
|
||||
onSelected: (_) => _selectObstruction(obstruction),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_selectedObstruction != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.deepOrangeAccent.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.losSelectedObstructionTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.losSelectedObstructionDetails(
|
||||
_formatHeightValue(
|
||||
_selectedObstruction!.obstructionMeters,
|
||||
isImperial,
|
||||
),
|
||||
heightUnit,
|
||||
_formatDistanceValue(
|
||||
_selectedObstruction!.distanceMeters,
|
||||
isImperial,
|
||||
),
|
||||
distanceUnit,
|
||||
_formatDistanceValue(
|
||||
segment!.totalDistanceMeters -
|
||||
_selectedObstruction!.distanceMeters,
|
||||
isImperial,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_selectedObstruction!.point.latitude.toStringAsFixed(5)}, '
|
||||
'${_selectedObstruction!.point.longitude.toStringAsFixed(5)}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
if (displayFrequencyMHz != null)
|
||||
Padding(
|
||||
@@ -605,6 +781,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
_showDisplayNodes = value;
|
||||
_sanitizeSelection();
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -655,6 +832,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
setState(() {
|
||||
_start = value;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
if (_start != null && _end != null) {
|
||||
_runLos();
|
||||
@@ -670,6 +848,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
setState(() {
|
||||
_end = value;
|
||||
_result = null;
|
||||
_selectedObstruction = null;
|
||||
});
|
||||
if (_start != null && _end != null) {
|
||||
_runLos();
|
||||
@@ -769,6 +948,179 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
return _result!.segments.first.result;
|
||||
}
|
||||
|
||||
List<LineOfSightObstruction> _primaryObstructions() {
|
||||
return _primarySegmentResult()?.obstructions ?? const [];
|
||||
}
|
||||
|
||||
LineOfSightObstruction? _defaultObstructionFor(
|
||||
LineOfSightPathResult? result,
|
||||
) {
|
||||
if (result == null || result.segments.isEmpty) return null;
|
||||
final obstructions = result.segments.first.result.obstructions;
|
||||
if (obstructions.isEmpty) return null;
|
||||
return obstructions.reduce(
|
||||
(current, next) =>
|
||||
next.obstructionMeters > current.obstructionMeters ? next : current,
|
||||
);
|
||||
}
|
||||
|
||||
void _selectObstruction(LineOfSightObstruction obstruction) {
|
||||
setState(() {
|
||||
_selectedObstruction = obstruction;
|
||||
});
|
||||
}
|
||||
|
||||
String _formatDistanceValue(double meters, bool isImperial) {
|
||||
final value = isImperial ? (meters / 1000.0) * _kmToMiles : meters / 1000.0;
|
||||
return value.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
String _formatHeightValue(double meters, bool isImperial) {
|
||||
final value = isImperial ? meters * _metersToFeet : meters;
|
||||
return value.toStringAsFixed(1);
|
||||
}
|
||||
|
||||
String _obstructionChipLabel(
|
||||
LineOfSightObstruction obstruction,
|
||||
bool isImperial,
|
||||
) {
|
||||
final distanceUnit = isImperial ? 'mi' : 'km';
|
||||
final heightUnit = isImperial ? 'ft' : 'm';
|
||||
return context.l10n.losBlockedSpotChip(
|
||||
_formatDistanceValue(obstruction.distanceMeters, isImperial),
|
||||
distanceUnit,
|
||||
_formatHeightValue(obstruction.obstructionMeters, isImperial),
|
||||
heightUnit,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileView(
|
||||
LineOfSightResult segment,
|
||||
String distanceUnit,
|
||||
String heightUnit,
|
||||
bool isImperial,
|
||||
) {
|
||||
if (segment.samples.length < 2) {
|
||||
return SizedBox(
|
||||
height: 160,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _LosProfilePainter(
|
||||
samples: segment.samples,
|
||||
distanceUnit: distanceUnit,
|
||||
heightUnit: heightUnit,
|
||||
badgeTextStyle:
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
) ??
|
||||
const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
terrainLabel: context.l10n.losLegendTerrain,
|
||||
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||
selectedSampleIndex: _selectedObstruction?.sampleIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 160,
|
||||
width: double.infinity,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final size = Size(constraints.maxWidth, 160);
|
||||
final geometry = _LosProfileGeometry(
|
||||
samples: segment.samples,
|
||||
size: size,
|
||||
);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: _LosProfilePainter(
|
||||
samples: segment.samples,
|
||||
distanceUnit: distanceUnit,
|
||||
heightUnit: heightUnit,
|
||||
badgeTextStyle:
|
||||
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
) ??
|
||||
const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
terrainLabel: context.l10n.losLegendTerrain,
|
||||
losBeamLabel: context.l10n.losLegendLosBeam,
|
||||
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
|
||||
selectedSampleIndex: _selectedObstruction?.sampleIndex,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final obstruction in segment.obstructions)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final sample = segment.samples[obstruction.sampleIndex];
|
||||
final position = geometry.mapPoint(
|
||||
sample.distanceMeters,
|
||||
sample.terrainMeters,
|
||||
);
|
||||
final isSelected =
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex;
|
||||
final markerSize = isSelected ? 18.0 : 14.0;
|
||||
final left = (position.dx - markerSize / 2)
|
||||
.clamp(0.0, math.max(0.0, size.width - markerSize))
|
||||
.toDouble();
|
||||
final top = (position.dy - markerSize / 2)
|
||||
.clamp(0.0, math.max(0.0, size.height - markerSize))
|
||||
.toDouble();
|
||||
return Positioned(
|
||||
left: left,
|
||||
top: top,
|
||||
child: Tooltip(
|
||||
message: _obstructionChipLabel(obstruction, isImperial),
|
||||
child: GestureDetector(
|
||||
onTap: () => _selectObstruction(obstruction),
|
||||
child: Container(
|
||||
width: markerSize,
|
||||
height: markerSize,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Colors.amberAccent
|
||||
: Colors.deepOrangeAccent,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: Colors.black87,
|
||||
width: isSelected ? 2 : 1.5,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black45, blurRadius: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _profileStats(LineOfSightResult result, bool isImperial) {
|
||||
final distance = isImperial
|
||||
? (result.totalDistanceMeters / 1000.0) * _kmToMiles
|
||||
@@ -820,8 +1172,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
return polylines;
|
||||
}
|
||||
|
||||
List<Marker> _buildMarkers(List<LineOfSightEndpoint> endpoints) {
|
||||
List<Marker> _buildMarkers(
|
||||
List<LineOfSightEndpoint> endpoints,
|
||||
List<LineOfSightObstruction> obstructions,
|
||||
) {
|
||||
return [
|
||||
for (final obstruction in obstructions)
|
||||
Marker(
|
||||
point: obstruction.point,
|
||||
width: 52,
|
||||
height: 52,
|
||||
child: GestureDetector(
|
||||
onTap: () => _selectObstruction(obstruction),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width:
|
||||
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
|
||||
? 36
|
||||
: 24,
|
||||
height:
|
||||
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
|
||||
? 36
|
||||
: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.transparent,
|
||||
border: Border.all(
|
||||
color:
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex
|
||||
? Colors.amberAccent
|
||||
: Colors.deepOrangeAccent,
|
||||
width:
|
||||
_selectedObstruction?.sampleIndex ==
|
||||
obstruction.sampleIndex
|
||||
? 4
|
||||
: 3,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final endpoint in endpoints)
|
||||
Marker(
|
||||
point: endpoint.point,
|
||||
@@ -1010,6 +1405,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LosProfileGeometry {
|
||||
static const horizontalPadding = 12.0;
|
||||
static const verticalPadding = 12.0;
|
||||
|
||||
final List<LineOfSightSample> samples;
|
||||
final Size size;
|
||||
late final double minY = samples
|
||||
.map(
|
||||
(s) => math.min(
|
||||
math.min(s.terrainMeters, s.lineHeightMeters),
|
||||
s.refractedHeightMeters,
|
||||
),
|
||||
)
|
||||
.reduce(math.min);
|
||||
late final double maxY = samples
|
||||
.map(
|
||||
(s) => math.max(
|
||||
math.max(s.terrainMeters, s.lineHeightMeters),
|
||||
s.refractedHeightMeters,
|
||||
),
|
||||
)
|
||||
.reduce(math.max);
|
||||
late final double ySpan = math.max(1.0, maxY - minY);
|
||||
late final double maxDist = math.max(1.0, samples.last.distanceMeters);
|
||||
late final double chartWidth = math.max(
|
||||
1.0,
|
||||
size.width - horizontalPadding * 2,
|
||||
);
|
||||
late final double chartHeight = math.max(
|
||||
1.0,
|
||||
size.height - verticalPadding * 2,
|
||||
);
|
||||
|
||||
_LosProfileGeometry({required this.samples, required this.size});
|
||||
|
||||
Offset mapPoint(double distanceMeters, double elevationMeters) {
|
||||
final px = horizontalPadding + (distanceMeters / maxDist) * chartWidth;
|
||||
final py =
|
||||
size.height -
|
||||
verticalPadding -
|
||||
((elevationMeters - minY) / ySpan) * chartHeight;
|
||||
return Offset(px, py);
|
||||
}
|
||||
}
|
||||
|
||||
class _LosProfilePainter extends CustomPainter {
|
||||
final List<LineOfSightSample> samples;
|
||||
final String distanceUnit;
|
||||
@@ -1018,6 +1458,7 @@ class _LosProfilePainter extends CustomPainter {
|
||||
final String terrainLabel;
|
||||
final String losBeamLabel;
|
||||
final String radioHorizonLabel;
|
||||
final int? selectedSampleIndex;
|
||||
|
||||
const _LosProfilePainter({
|
||||
required this.samples,
|
||||
@@ -1027,6 +1468,7 @@ class _LosProfilePainter extends CustomPainter {
|
||||
required this.terrainLabel,
|
||||
required this.losBeamLabel,
|
||||
required this.radioHorizonLabel,
|
||||
this.selectedSampleIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1212,6 +1654,32 @@ class _LosProfilePainter extends CustomPainter {
|
||||
..color = horizonFillColor
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
|
||||
if (selectedSampleIndex != null &&
|
||||
selectedSampleIndex! >= 0 &&
|
||||
selectedSampleIndex! < samples.length) {
|
||||
final selectedSample = samples[selectedSampleIndex!];
|
||||
final selectedPoint = mapPoint(
|
||||
selectedSample.distanceMeters,
|
||||
selectedSample.terrainMeters,
|
||||
);
|
||||
canvas.drawLine(
|
||||
Offset(selectedPoint.dx, verticalPadding),
|
||||
Offset(selectedPoint.dx, size.height - verticalPadding),
|
||||
Paint()
|
||||
..color = Colors.amberAccent.withValues(alpha: 0.7)
|
||||
..strokeWidth = 1.5,
|
||||
);
|
||||
canvas.drawCircle(selectedPoint, 7, Paint()..color = Colors.amberAccent);
|
||||
canvas.drawCircle(
|
||||
selectedPoint,
|
||||
8.5,
|
||||
Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1222,7 +1690,8 @@ class _LosProfilePainter extends CustomPainter {
|
||||
oldDelegate.badgeTextStyle != badgeTextStyle ||
|
||||
oldDelegate.terrainLabel != terrainLabel ||
|
||||
oldDelegate.losBeamLabel != losBeamLabel ||
|
||||
oldDelegate.radioHorizonLabel != radioHorizonLabel;
|
||||
oldDelegate.radioHorizonLabel != radioHorizonLabel ||
|
||||
oldDelegate.selectedSampleIndex != selectedSampleIndex;
|
||||
}
|
||||
|
||||
void _drawUnitBadge(Canvas canvas, Size size) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -8,6 +9,7 @@ import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class MapCacheScreen extends StatefulWidget {
|
||||
const MapCacheScreen({super.key});
|
||||
@@ -17,6 +19,9 @@ class MapCacheScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
LatLngBounds? _selectedBounds;
|
||||
@@ -42,6 +47,61 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView() {
|
||||
final bounds = _selectedBounds;
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_mapController.move(const LatLng(0, 0), 2.0);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls() {
|
||||
return Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final bounds = MapTileCacheService.boundsFromJson(settings.mapCacheBounds);
|
||||
@@ -112,15 +172,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
Future<void> _startDownload() async {
|
||||
final bounds = _selectedBounds;
|
||||
if (bounds == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.mapCache_selectAreaFirst)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_selectAreaFirst),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_estimatedTiles == 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.mapCache_noTilesToDownload)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_noTilesToDownload),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -182,9 +244,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
result.failed,
|
||||
)
|
||||
: context.l10n.mapCache_cachedTiles(result.downloaded);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
showDismissibleSnackBar(context, content: Text(message));
|
||||
}
|
||||
|
||||
Future<void> _clearCache() async {
|
||||
@@ -210,8 +270,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
await cacheService.clearCache();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.mapCache_offlineCacheCleared)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_offlineCacheCleared),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,6 +281,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final selectedBounds = _selectedBounds;
|
||||
final l10n = context.l10n;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final progressValue = _estimatedTiles == 0
|
||||
? 0.0
|
||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||
@@ -236,11 +298,24 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: const MapOptions(
|
||||
initialCenter: LatLng(0, 0),
|
||||
options: MapOptions(
|
||||
initialCenter: const LatLng(0, 0),
|
||||
initialZoom: 2.0,
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -263,6 +338,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop) _buildDesktopMapControls(),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
|
||||
+325
-93
@@ -1,6 +1,5 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -16,6 +15,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../services/map_marker_service.dart';
|
||||
@@ -29,6 +29,7 @@ import 'chat_screen.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import '../widgets/repeater_login_dialog.dart';
|
||||
import '../widgets/room_login_dialog.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'repeater_hub_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'line_of_sight_map_screen.dart';
|
||||
@@ -36,6 +37,7 @@ import 'line_of_sight_map_screen.dart';
|
||||
class MapScreen extends StatefulWidget {
|
||||
final LatLng? highlightPosition;
|
||||
final String? highlightLabel;
|
||||
final String? highlightMarkerKey;
|
||||
final double highlightZoom;
|
||||
final bool hideBackButton;
|
||||
|
||||
@@ -43,6 +45,7 @@ class MapScreen extends StatefulWidget {
|
||||
super.key,
|
||||
this.highlightPosition,
|
||||
this.highlightLabel,
|
||||
this.highlightMarkerKey,
|
||||
this.highlightZoom = 15.0,
|
||||
this.hideBackButton = false,
|
||||
});
|
||||
@@ -54,6 +57,8 @@ class MapScreen extends StatefulWidget {
|
||||
class _MapScreenState extends State<MapScreen> {
|
||||
// Zoom level at which node labels start to appear
|
||||
static const double _labelZoomThreshold = 14.0;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
@@ -64,6 +69,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
bool _hasInitializedMap = false;
|
||||
bool _removedMarkersLoaded = false;
|
||||
final List<int> _pathTrace = [];
|
||||
final List<Contact> _pathTraceContacts = [];
|
||||
final List<LatLng> _points = [];
|
||||
final List<Polyline> _polylines = [];
|
||||
bool _legendExpanded = false;
|
||||
@@ -92,6 +98,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_removedMarkerIds = ids;
|
||||
_removedMarkersLoaded = true;
|
||||
});
|
||||
// If this screen was opened to highlight a marker, and that marker
|
||||
// was previously removed, re-enable it now that we've loaded the saved
|
||||
// removed IDs.
|
||||
if (widget.highlightMarkerKey != null &&
|
||||
_removedMarkerIds.contains(widget.highlightMarkerKey)) {
|
||||
final updated = Set<String>.from(_removedMarkerIds);
|
||||
updated.remove(widget.highlightMarkerKey);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_removedMarkerIds = updated;
|
||||
});
|
||||
await _markerService.saveRemovedIds(updated);
|
||||
}
|
||||
}
|
||||
|
||||
bool _checkLocationPlausibility(double lat, double lon) {
|
||||
@@ -132,11 +151,62 @@ class _MapScreenState extends State<MapScreen> {
|
||||
return zoom.clamp(4.0, 15.0);
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls(
|
||||
BuildContext context, {
|
||||
required LatLng center,
|
||||
required double zoom,
|
||||
required bool hasPathSelector,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: 16,
|
||||
top: hasPathSelector ? null : 16,
|
||||
bottom: hasPathSelector ? 16 : null,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: () => _mapController.move(center, zoom),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
||||
builder: (context, connector, settingsService, pathHistory, child) {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final settings = settingsService.settings;
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
@@ -227,6 +297,24 @@ class _MapScreenState extends State<MapScreen> {
|
||||
: <Polyline>[],
|
||||
);
|
||||
|
||||
// Collect polylines for shared markers' history with dashed lines
|
||||
final List<Polyline> sharedMarkerPolylines = [];
|
||||
for (final marker in sharedMarkers) {
|
||||
if (marker.history.isNotEmpty) {
|
||||
final points = List<LatLng>.from(marker.history);
|
||||
points.add(marker.position);
|
||||
sharedMarkerPolylines.add(
|
||||
Polyline(
|
||||
points: points,
|
||||
color: marker.isChannel
|
||||
? (marker.isPublicChannel ? Colors.orange : Colors.purple)
|
||||
: Colors.blue,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate center and zoom of all nodes, or default to (0, 0)
|
||||
LatLng center = const LatLng(0, 0);
|
||||
double initialZoom = 10.0;
|
||||
@@ -415,10 +503,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
options: MapOptions(
|
||||
initialCenter: center,
|
||||
initialZoom: initialZoom,
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onTap: (_, latLng) {
|
||||
if (_isSelectingPoi) {
|
||||
@@ -473,6 +571,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
||||
PolylineLayer(polylines: _polylines),
|
||||
if (sharedMarkerPolylines.isNotEmpty)
|
||||
PolylineLayer(polylines: sharedMarkerPolylines),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
if (highlightPosition != null)
|
||||
@@ -488,7 +588,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
if (!settings.mapShowOverlaps)
|
||||
..._buildGuessedMarker(
|
||||
guessedLocations,
|
||||
showLabels: _showNodeLabels,
|
||||
@@ -560,6 +660,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
sharedMarkers.length,
|
||||
guessedLocations.length,
|
||||
),
|
||||
if (isDesktop)
|
||||
_buildDesktopMapControls(
|
||||
context,
|
||||
center: center,
|
||||
zoom: initialZoom,
|
||||
hasPathSelector: _isBuildingPathTrace,
|
||||
),
|
||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||
],
|
||||
),
|
||||
@@ -788,17 +895,26 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final markers = <Marker>[];
|
||||
|
||||
for (final guess in guessed) {
|
||||
if (guess.contact.type == advTypeChat && _isBuildingPathTrace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final color = _getNodeColor(guess.contact.type);
|
||||
final marker = Marker(
|
||||
point: guess.position,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showNodeInfo(
|
||||
context,
|
||||
guess.contact,
|
||||
guessedPosition: guess.position,
|
||||
),
|
||||
onLongPress: () => _isBuildingPathTrace
|
||||
? _showNodeInfo(context, guess.contact)
|
||||
: null,
|
||||
onTap: () => _isBuildingPathTrace
|
||||
? _addToPath(context, guess.contact, position: guess.position)
|
||||
: _showNodeInfo(
|
||||
context,
|
||||
guess.contact,
|
||||
guessedPosition: guess.position,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -870,23 +986,29 @@ class _MapScreenState extends State<MapScreen> {
|
||||
addContact = true;
|
||||
}
|
||||
|
||||
final hasOverlap = contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKeyHex != contact.publicKeyHex &&
|
||||
c.publicKey.first == contact.publicKey.first &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||
(contact.type == advTypeRepeater ||
|
||||
contact.type == advTypeRoom),
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (hasOverlap == null &&
|
||||
settings.mapShowOverlaps &&
|
||||
!_isBuildingPathTrace) {
|
||||
if (contact.type == advTypeChat && _isBuildingPathTrace) {
|
||||
addContact = false;
|
||||
}
|
||||
|
||||
if (settings.mapShowOverlaps) {
|
||||
final hasOverlap = contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKeyHex != contact.publicKeyHex &&
|
||||
c.publicKey.first == contact.publicKey.first &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||
(contact.type == advTypeRepeater ||
|
||||
contact.type == advTypeRoom),
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (hasOverlap == null &&
|
||||
settings.mapShowOverlaps &&
|
||||
!_isBuildingPathTrace) {
|
||||
addContact = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (addContact) {
|
||||
filtered.add(contact);
|
||||
}
|
||||
@@ -1222,28 +1344,39 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
List<_SharedMarker> _collectSharedMarkers(MeshCoreConnector connector) {
|
||||
final markers = <_SharedMarker>[];
|
||||
// Build a _SharedMarker per message (history empty), grouped by dedupe key.
|
||||
// Afterwards pick the latest per key and fill its history from older ones.
|
||||
final updatesByKey = <String, List<_SharedMarker>>{};
|
||||
final selfName = connector.selfName ?? 'Me';
|
||||
|
||||
void addUpdate(_SharedMarker update) {
|
||||
(updatesByKey[update.id] ??= <_SharedMarker>[]).add(update);
|
||||
}
|
||||
|
||||
for (final contact in connector.contacts) {
|
||||
final messages = connector.getMessages(contact);
|
||||
for (final message in messages) {
|
||||
final payload = _parseMarkerText(message.text);
|
||||
final payload = parseMarkerText(message.text);
|
||||
if (payload == null) continue;
|
||||
final fromName = message.isOutgoing ? selfName : contact.name;
|
||||
final id = _buildMarkerId(
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: contact.publicKeyHex,
|
||||
timestamp: message.timestamp,
|
||||
text: message.text,
|
||||
label: payload.label,
|
||||
fromName: fromName,
|
||||
flags: payload.flags,
|
||||
isChannel: false,
|
||||
);
|
||||
markers.add(
|
||||
addUpdate(
|
||||
_SharedMarker(
|
||||
id: id,
|
||||
id: key,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
label: payload.label.isEmpty
|
||||
? context.l10n.map_sharedPin
|
||||
: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: fromName,
|
||||
sourceLabel: contact.name,
|
||||
timestamp: message.timestamp,
|
||||
isChannel: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
@@ -1255,23 +1388,28 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final isPublic = _isPublicChannel(channel);
|
||||
final messages = connector.getChannelMessages(channel);
|
||||
for (final message in messages) {
|
||||
final payload = _parseMarkerText(message.text);
|
||||
final payload = parseMarkerText(message.text);
|
||||
if (payload == null) continue;
|
||||
final id = _buildMarkerId(
|
||||
final key = buildSharedMarkerKey(
|
||||
sourceId: 'channel:${channel.index}',
|
||||
timestamp: message.timestamp,
|
||||
text: message.text,
|
||||
label: payload.label,
|
||||
fromName: message.senderName,
|
||||
flags: payload.flags,
|
||||
isChannel: true,
|
||||
);
|
||||
markers.add(
|
||||
addUpdate(
|
||||
_SharedMarker(
|
||||
id: id,
|
||||
id: key,
|
||||
position: payload.position,
|
||||
label: payload.label,
|
||||
label: payload.label.isEmpty
|
||||
? context.l10n.map_sharedPin
|
||||
: payload.label,
|
||||
flags: payload.flags,
|
||||
fromName: message.senderName,
|
||||
sourceLabel: channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name,
|
||||
timestamp: message.timestamp,
|
||||
isChannel: true,
|
||||
isPublicChannel: isPublic,
|
||||
),
|
||||
@@ -1279,38 +1417,27 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
final markers = <_SharedMarker>[];
|
||||
updatesByKey.forEach((_, updates) {
|
||||
updates.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
final latest = updates.last;
|
||||
// History: older positions, drop consecutive duplicates at same position.
|
||||
final history = <LatLng>[];
|
||||
for (var i = 0; i < updates.length - 1; i++) {
|
||||
final p = updates[i].position;
|
||||
if (history.isEmpty ||
|
||||
history.last.latitude != p.latitude ||
|
||||
history.last.longitude != p.longitude) {
|
||||
history.add(p);
|
||||
}
|
||||
}
|
||||
markers.add(latest.copyWithHistory(history));
|
||||
});
|
||||
|
||||
markers.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
return markers;
|
||||
}
|
||||
|
||||
_MarkerPayload? _parseMarkerText(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (!trimmed.startsWith('m:')) return null;
|
||||
|
||||
final parts = trimmed.substring(2).split('|');
|
||||
if (parts.isEmpty) return null;
|
||||
final coords = parts[0].split(',');
|
||||
if (coords.length != 2) return null;
|
||||
final lat = double.tryParse(coords[0].trim());
|
||||
final lon = double.tryParse(coords[1].trim());
|
||||
if (lat == null || lon == null) return null;
|
||||
|
||||
final label = parts.length > 1 ? parts[1].trim() : '';
|
||||
final flags = parts.length > 2 ? parts[2].trim() : '';
|
||||
return _MarkerPayload(
|
||||
position: LatLng(lat, lon),
|
||||
label: label.isEmpty ? context.l10n.map_sharedPin : label,
|
||||
flags: flags,
|
||||
);
|
||||
}
|
||||
|
||||
String _buildMarkerId({
|
||||
required String sourceId,
|
||||
required DateTime timestamp,
|
||||
required String text,
|
||||
}) {
|
||||
return '$sourceId|${timestamp.millisecondsSinceEpoch}|$text';
|
||||
}
|
||||
|
||||
Marker _buildSharedMarker(_SharedMarker marker) {
|
||||
final markerColor = marker.isChannel
|
||||
? (marker.isPublicChannel ? Colors.orange : Colors.purple)
|
||||
@@ -1320,7 +1447,15 @@ class _MapScreenState extends State<MapScreen> {
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showMarkerInfo(marker),
|
||||
onTap: () async {
|
||||
if (_removedMarkerIds.contains(marker.id)) {
|
||||
setState(() {
|
||||
_removedMarkerIds.remove(marker.id);
|
||||
});
|
||||
await _markerService.saveRemovedIds(_removedMarkerIds);
|
||||
}
|
||||
_showMarkerInfo(marker);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -1350,13 +1485,16 @@ class _MapScreenState extends State<MapScreen> {
|
||||
context: context,
|
||||
builder: (context) => RepeaterLoginDialog(
|
||||
repeater: repeater,
|
||||
onLogin: (password) {
|
||||
onLogin: (password, isAdmin) {
|
||||
// Navigate to repeater hub screen after successful login
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
RepeaterHubScreen(repeater: repeater, password: password),
|
||||
builder: (context) => RepeaterHubScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
isAdmin: isAdmin,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1369,12 +1507,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
context: context,
|
||||
builder: (context) => RoomLoginDialog(
|
||||
room: room,
|
||||
onLogin: (password) {
|
||||
// Navigate to chat screen after successful login
|
||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
||||
// onLogin(password, isAdmin) isAdmin not used for room caht screen
|
||||
onLogin: (password, _) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
room.publicKeyHex,
|
||||
);
|
||||
connector.markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: room)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ChatScreen(contact: room, initialUnreadCount: unread),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -1404,23 +1549,29 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('Type', contact.typeLabel),
|
||||
_buildInfoRow('Path', contact.pathLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_type,
|
||||
contact.typeLabel(context.l10n),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
'Location',
|
||||
context.l10n.map_location,
|
||||
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
|
||||
)
|
||||
else if (guessedPosition != null)
|
||||
_buildInfoRow(
|
||||
'Est. Location',
|
||||
context.l10n.map_estLocation,
|
||||
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_lastSeen,
|
||||
_formatLastSeen(contact.lastSeen),
|
||||
),
|
||||
_buildInfoRow('Public Key', contact.publicKeyHex),
|
||||
_buildInfoRow(context.l10n.map_publicKey, contact.publicKeyHex),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -1435,11 +1586,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (!contact.isActive) {
|
||||
connector.importDiscoveredContact(contact);
|
||||
}
|
||||
final unread = connector.getUnreadCountForContactKey(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(contact: contact),
|
||||
builder: (context) => ChatScreen(
|
||||
contact: contact,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1521,7 +1678,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(marker.label),
|
||||
title: Text(
|
||||
marker.label.isEmpty ? context.l10n.map_sharedPin : marker.label,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1529,7 +1688,11 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_buildInfoRow(context.l10n.map_from, marker.fromName),
|
||||
_buildInfoRow(context.l10n.map_source, marker.sourceLabel),
|
||||
_buildInfoRow(
|
||||
'Location',
|
||||
context.l10n.map_sharedAt,
|
||||
_formatLastSeen(marker.timestamp),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.map_location,
|
||||
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
if (marker.flags.isNotEmpty)
|
||||
@@ -1643,7 +1806,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
|
||||
showDismissibleSnackBar(
|
||||
messenger.context,
|
||||
content: Text(successMsg),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -1665,8 +1831,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
required String flags,
|
||||
}) async {
|
||||
if (!connector.isConnected) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.map_connectToShareMarkers)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.map_connectToShareMarkers),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1690,6 +1857,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
String defaultLabel,
|
||||
) async {
|
||||
final controller = TextEditingController(text: defaultLabel);
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
@@ -1994,7 +2165,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
enabled: settings.mapKeyPrefixEnabled,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.map_publicKeyPrefix,
|
||||
hintText: 'e.g. ab12',
|
||||
hintText: context.l10n.map_publicKeyPrefixHint,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
@@ -2121,12 +2292,18 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _addToPath(BuildContext context, Contact contact) {
|
||||
void _addToPath(BuildContext context, Contact contact, {LatLng? position}) {
|
||||
setState(() {
|
||||
_pathTrace.add(
|
||||
contact.publicKey[0],
|
||||
); // Add first 16 bytes of public key to path trace
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
_pathTraceContacts.add(
|
||||
contact.copyWith(
|
||||
latitude: position?.latitude ?? contact.latitude,
|
||||
longitude: position?.longitude ?? contact.longitude,
|
||||
),
|
||||
); // Add contact to path trace contacts
|
||||
_points.add(position ?? LatLng(contact.latitude!, contact.longitude!));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2134,6 +2311,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = true;
|
||||
_pathTrace.clear();
|
||||
_pathTraceContacts.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
_points.add(position);
|
||||
@@ -2142,6 +2320,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
void _removePath() {
|
||||
setState(() {
|
||||
_pathTraceContacts.removeLast();
|
||||
_pathTrace.removeLast(); // Remove last node from path trace
|
||||
_points.removeLast(); // Remove last point from points list
|
||||
_polylines.clear(); // Clear polylines
|
||||
@@ -2201,6 +2380,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
pathHashByteWidth: hashW,
|
||||
pathContacts: _pathTraceContacts,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2246,8 +2426,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.map_pathTraceCancelled),
|
||||
);
|
||||
},
|
||||
tooltip: l10n.common_cancel,
|
||||
@@ -2275,18 +2456,50 @@ class _GuessedLocation {
|
||||
});
|
||||
}
|
||||
|
||||
class _MarkerPayload {
|
||||
class MarkerPayload {
|
||||
final LatLng position;
|
||||
final String label;
|
||||
final String flags;
|
||||
|
||||
_MarkerPayload({
|
||||
MarkerPayload({
|
||||
required this.position,
|
||||
required this.label,
|
||||
required this.flags,
|
||||
});
|
||||
}
|
||||
|
||||
/// Parse a shared marker text message of the form
|
||||
/// `m:<lat>,<lon>|<label>|<flags>` and return a [MarkerPayload].
|
||||
MarkerPayload? parseMarkerText(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(
|
||||
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|(.*)',
|
||||
).firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final lat = double.tryParse(match.group(1) ?? '');
|
||||
final lon = double.tryParse(match.group(2) ?? '');
|
||||
if (lat == null || lon == null) return null;
|
||||
final label = (match.group(3) ?? '').trim();
|
||||
final flags = (match.group(4) ?? '').trim();
|
||||
return MarkerPayload(position: LatLng(lat, lon), label: label, flags: flags);
|
||||
}
|
||||
|
||||
/// Build a normalized dedupe key for shared markers.
|
||||
/// Keeps the same algorithm previously present in both chat and map screens.
|
||||
String buildSharedMarkerKey({
|
||||
required String sourceId,
|
||||
required String label,
|
||||
required String fromName,
|
||||
required String flags,
|
||||
required bool isChannel,
|
||||
}) {
|
||||
final normalizedLabel = label.trim().toLowerCase();
|
||||
final normalizedFrom = fromName.trim().toLowerCase();
|
||||
final normalizedFlags = flags.trim().toLowerCase();
|
||||
final scope = isChannel ? 'ch' : 'dm';
|
||||
return '$scope|$sourceId|$normalizedFrom|$normalizedLabel|$normalizedFlags';
|
||||
}
|
||||
|
||||
class _SharedMarker {
|
||||
final String id;
|
||||
final LatLng position;
|
||||
@@ -2294,8 +2507,10 @@ class _SharedMarker {
|
||||
final String flags;
|
||||
final String fromName;
|
||||
final String sourceLabel;
|
||||
final DateTime timestamp;
|
||||
final bool isChannel;
|
||||
final bool isPublicChannel;
|
||||
final List<LatLng> history;
|
||||
|
||||
_SharedMarker({
|
||||
required this.id,
|
||||
@@ -2304,7 +2519,24 @@ class _SharedMarker {
|
||||
required this.flags,
|
||||
required this.fromName,
|
||||
required this.sourceLabel,
|
||||
required this.timestamp,
|
||||
required this.isChannel,
|
||||
required this.isPublicChannel,
|
||||
this.history = const [],
|
||||
});
|
||||
|
||||
_SharedMarker copyWithHistory(List<LatLng> newHistory) {
|
||||
return _SharedMarker(
|
||||
id: id,
|
||||
position: position,
|
||||
label: label,
|
||||
flags: flags,
|
||||
fromName: fromName,
|
||||
sourceLabel: sourceLabel,
|
||||
timestamp: timestamp,
|
||||
isChannel: isChannel,
|
||||
isPublicChannel: isPublicChannel,
|
||||
history: newHistory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/snr_indicator.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class NeighborsScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -142,7 +143,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
|
||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
final buffer = BufferReader(frame);
|
||||
final contacts = connector.allContacts;
|
||||
final contacts = connector.allContactsUnfiltered;
|
||||
try {
|
||||
final neighborCount = buffer.readUInt16LE();
|
||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||
@@ -163,11 +164,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
_neighborCount = neighborCount;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
@@ -224,11 +224,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_requestTimedOut),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_requestTimedOut),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
@@ -239,11 +238,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
_isLoaded = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+125
-13
@@ -56,6 +56,7 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
final bool reversePathAround;
|
||||
final Contact? targetContact;
|
||||
final int pathHashByteWidth;
|
||||
final List<Contact>? pathContacts;
|
||||
|
||||
const PathTraceMapScreen({
|
||||
super.key,
|
||||
@@ -66,6 +67,7 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
this.reversePathAround = false,
|
||||
this.targetContact,
|
||||
this.pathHashByteWidth = pathHashSize,
|
||||
this.pathContacts,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -74,7 +76,12 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
|
||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
static const double _mapMinZoom = 2.0;
|
||||
static const double _mapMaxZoom = 18.0;
|
||||
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Timer? _timeoutTimer;
|
||||
|
||||
@@ -112,11 +119,74 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mapController.dispose();
|
||||
_frameSubscription?.cancel();
|
||||
_timeoutTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isDesktopPlatform(TargetPlatform platform) {
|
||||
return platform == TargetPlatform.linux ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.macOS;
|
||||
}
|
||||
|
||||
void _zoomMapBy(double delta) {
|
||||
final camera = _mapController.camera;
|
||||
final nextZoom = (camera.zoom + delta)
|
||||
.clamp(_mapMinZoom, _mapMaxZoom)
|
||||
.toDouble();
|
||||
_mapController.move(camera.center, nextZoom);
|
||||
}
|
||||
|
||||
void _resetMapView() {
|
||||
final bounds = _bounds;
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final center = _initialCenter;
|
||||
if (center != null) {
|
||||
_mapController.move(center, _initialZoom);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDesktopMapControls() {
|
||||
return Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Zoom in',
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: 'Zoom out',
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.my_location),
|
||||
tooltip: 'Center map',
|
||||
onPressed: _resetMapView,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List buildPath(Uint8List pathBytes) {
|
||||
Uint8List traceBytes;
|
||||
|
||||
@@ -266,17 +336,43 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
final contacts = connector.allContacts;
|
||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
Contact lastContact = Contact(
|
||||
path: Uint8List(0),
|
||||
pathLength: 0,
|
||||
publicKey: connector.selfPublicKey ?? Uint8List(0),
|
||||
name: context.l10n.pathTrace_you,
|
||||
type: advTypeChat,
|
||||
latitude: connector.selfLatitude,
|
||||
longitude: connector.selfLongitude,
|
||||
lastSeen: DateTime.now(),
|
||||
);
|
||||
if (widget.pathContacts != null) {
|
||||
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
||||
} else {
|
||||
final contacts = connector.allContactsUnfiltered;
|
||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
if (lastContact.latitude != null &&
|
||||
lastContact.longitude != null &&
|
||||
repeater.hasLocation &&
|
||||
lastContact.hasLocation &&
|
||||
Distance().distance(
|
||||
LatLng(lastContact.latitude!, lastContact.longitude!),
|
||||
LatLng(repeater.latitude!, repeater.longitude!),
|
||||
) >
|
||||
_maxRepeaterMatchDistanceMeters) {
|
||||
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
|
||||
}
|
||||
}
|
||||
});
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
lastContact = repeater;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For hops with no GPS contact, infer position from other contacts
|
||||
// with known GPS that share the same last-hop byte.
|
||||
@@ -489,6 +585,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
if (_hasData)
|
||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||
if (_hasData && _isDesktopPlatform(defaultTargetPlatform))
|
||||
_buildDesktopMapControls(),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
@@ -771,10 +869,24 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
MapTileCacheService tileCache,
|
||||
Contact? target,
|
||||
) {
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
|
||||
cursorKeyboardRotationOptions:
|
||||
CursorKeyboardRotationOptions.disabled(),
|
||||
keyboardOptions: isDesktop
|
||||
? const KeyboardOptions(
|
||||
enableArrowKeysPanning: true,
|
||||
enableWASDPanning: true,
|
||||
enableRFZooming: true,
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
initialCenter: _initialCenter!,
|
||||
initialZoom: _initialZoom,
|
||||
initialCameraFit: _bounds == null
|
||||
@@ -784,8 +896,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (shouldShow != _showNodeLabels && mounted) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../widgets/debug_frame_viewer.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterCliScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -35,13 +36,15 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
|
||||
// Common commands for quick access
|
||||
late final List<Map<String, String>> _quickCommands = [
|
||||
{'labelKey': 'advertise', 'command': 'advert'},
|
||||
{'labelKey': 'getName', 'command': 'get name'},
|
||||
{'labelKey': 'getRadio', 'command': 'get radio'},
|
||||
{'labelKey': 'getTx', 'command': 'get tx'},
|
||||
{'labelKey': 'discovery', 'command': 'discover.neighbors'},
|
||||
{'labelKey': 'neighbors', 'command': 'neighbors'},
|
||||
{'labelKey': 'version', 'command': 'ver'},
|
||||
{'labelKey': 'advertise', 'command': 'advert'},
|
||||
{'labelKey': 'clock', 'command': 'clock'},
|
||||
{'labelKey': 'clock sync', 'command': 'clock sync'},
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -334,8 +337,9 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
if (_commandController.text.trim().isNotEmpty) {
|
||||
_sendCommand(showDebug: true);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.repeater_enterCommandFirst)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_enterCommandFirst),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -407,6 +411,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
return l10n.repeater_cliQuickAdvertise;
|
||||
case 'clock':
|
||||
return l10n.repeater_cliQuickClock;
|
||||
case 'clock sync':
|
||||
return l10n.repeater_cliQuickClockSync;
|
||||
case 'discovery':
|
||||
return l10n.repeater_cliQuickDiscovery;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../l10n/contact_localization.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import 'repeater_status_screen.dart';
|
||||
import 'repeater_cli_screen.dart';
|
||||
@@ -13,11 +14,13 @@ import 'neighbors_screen.dart';
|
||||
class RepeaterHubScreen extends StatelessWidget {
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
final bool isAdmin;
|
||||
|
||||
const RepeaterHubScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
required this.isAdmin,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -33,11 +36,18 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
repeater.type == advTypeRepeater
|
||||
? l10n.repeater_management
|
||||
: l10n.room_management,
|
||||
),
|
||||
if (isAdmin)
|
||||
Text(
|
||||
repeater.type == advTypeRepeater
|
||||
? l10n.repeater_management
|
||||
: l10n.room_management,
|
||||
),
|
||||
if (!isAdmin)
|
||||
Text(
|
||||
repeater.type == advTypeRepeater
|
||||
? l10n.repeater_guest
|
||||
: l10n.room_guest,
|
||||
),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(
|
||||
@@ -84,7 +94,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
repeater.pathLabel,
|
||||
repeater.pathLabel(context.l10n),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
if (repeater.hasLocation) ...[
|
||||
@@ -113,64 +123,67 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.battery_full),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.appSettings_batteryChemistry,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
if (isAdmin)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.battery_full),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.appSettings_batteryChemistry,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: chemistry,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
],
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
settingsService.setBatteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'nmc',
|
||||
child: Text(l10n.appSettings_batteryNmc),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: chemistry,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lifepo4',
|
||||
child: Text(l10n.appSettings_batteryLifepo4),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo',
|
||||
child: Text(l10n.appSettings_batteryLipo),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
settingsService.setBatteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'nmc',
|
||||
child: Text(l10n.appSettings_batteryNmc),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lifepo4',
|
||||
child: Text(l10n.appSettings_batteryLifepo4),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo',
|
||||
child: Text(l10n.appSettings_batteryLipo),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.repeater_managementTools,
|
||||
isAdmin
|
||||
? l10n.repeater_managementTools
|
||||
: l10n.repeater_guestTools,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -210,26 +223,27 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (isAdmin) const SizedBox(height: 12),
|
||||
// CLI button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.terminal,
|
||||
title: l10n.repeater_cli,
|
||||
subtitle: l10n.repeater_cliSubtitle,
|
||||
color: Colors.green,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterCliScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
if (isAdmin)
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.terminal,
|
||||
title: l10n.repeater_cli,
|
||||
subtitle: l10n.repeater_cliSubtitle,
|
||||
color: Colors.green,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterCliScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Neighbors button
|
||||
_buildManagementCard(
|
||||
@@ -248,26 +262,27 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (isAdmin) const SizedBox(height: 12),
|
||||
// Settings button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.settings,
|
||||
title: l10n.repeater_settings,
|
||||
subtitle: l10n.repeater_settingsSubtitle,
|
||||
color: Colors.deepOrange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterSettingsScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
if (isAdmin)
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.settings,
|
||||
title: l10n.repeater_settings,
|
||||
subtitle: l10n.repeater_settingsSubtitle,
|
||||
color: Colors.deepOrange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterSettingsScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,7 +8,9 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterSettingsScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -25,6 +27,8 @@ class RepeaterSettingsScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
final StorageService _storage = StorageService();
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _hasChanges = false;
|
||||
bool _refreshingBasic = false;
|
||||
@@ -59,6 +63,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
bool _repeatEnabled = true;
|
||||
bool _allowReadOnly = true;
|
||||
bool _privacyMode = false;
|
||||
bool _autoClockSyncAfterLogin = false;
|
||||
|
||||
// Advertisement settings
|
||||
bool _advertEnable = true;
|
||||
@@ -464,18 +469,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
if (mounted) {
|
||||
if (successCount > 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.repeater_refreshed(label)),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_refreshed(label)),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.repeater_errorRefreshing(label)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_errorRefreshing(label)),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -566,6 +569,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_lonController.text = widget.repeater.longitude?.toString() ?? '';
|
||||
}
|
||||
});
|
||||
|
||||
final autoClockSync = await _storage
|
||||
.getRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
widget.repeater.publicKeyHex,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_autoClockSyncAfterLogin = autoClockSync;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveSettings() async {
|
||||
@@ -653,11 +665,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.repeater_settingsSaved),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_settingsSaved),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -666,13 +677,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.repeater_errorSavingSettings(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.repeater_errorSavingSettings(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1139,6 +1149,21 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
onRefresh: _refreshAllowReadOnly,
|
||||
refreshTooltip: l10n.repeater_refreshGuestAccess,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(l10n.repeater_clockSyncAfterLogin),
|
||||
subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle),
|
||||
value: _autoClockSyncAfterLogin,
|
||||
onChanged: (value) async {
|
||||
setState(() {
|
||||
_autoClockSyncAfterLogin = value;
|
||||
});
|
||||
await _storage.setRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
widget.repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
// Privacy mode - hidden until fully implemented
|
||||
// _buildFeatureToggleRow(
|
||||
// title: l10n.repeater_privacyMode,
|
||||
@@ -1401,9 +1426,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
if (command == 'erase') {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly)));
|
||||
content: Text(l10n.repeater_eraseSerialOnly),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1425,17 +1451,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
await connector.sendFrame(frame);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.repeater_commandSent(command))),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_commandSent(command)),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterStatusScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -309,11 +310,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.repeater_statusRequestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_statusRequestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
@@ -323,13 +323,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.repeater_errorLoadingStatus(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
_recordStatusResult(false);
|
||||
|
||||
@@ -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 '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
@@ -317,11 +318,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/utils/gpx_export.dart';
|
||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||
@@ -8,12 +9,29 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/radio_settings.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../widgets/app_bar.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'app_settings_screen.dart';
|
||||
import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
|
||||
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
||||
/// to the UI enum range (always 5-8).
|
||||
int _toUiCodingRate(int deviceCr) {
|
||||
return deviceCr <= 4 ? deviceCr + 4 : deviceCr;
|
||||
}
|
||||
|
||||
/// Convert UI coding-rate value (5-8) back to firmware encoding.
|
||||
/// Uses the current device CR to detect which encoding the firmware expects.
|
||||
int _toDeviceCodingRate(int uiCr, int? deviceCr) {
|
||||
if (deviceCr != null && deviceCr <= 4) {
|
||||
return uiCr - 4;
|
||||
}
|
||||
return uiCr;
|
||||
}
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@@ -323,9 +341,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
title: Text("Delete All Paths"),
|
||||
title: Text(l10n.settings_deleteAllPaths),
|
||||
subtitle: Text(
|
||||
"Clear all path data from contacts.",
|
||||
l10n.settings_deleteAllPathsSubtitle,
|
||||
style: TextStyle(color: Colors.red[700]),
|
||||
),
|
||||
onTap: () => connector.deleteAllPaths(),
|
||||
@@ -387,8 +405,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.bluetooth_outlined),
|
||||
title: Text(l10n.settings_bleDebugLog),
|
||||
subtitle: Text(l10n.settings_bleDebugLogSubtitle),
|
||||
title: Text(l10n.settings_companionDebugLog),
|
||||
subtitle: Text(l10n.settings_companionDebugLogSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@@ -496,8 +514,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await connector.setNodeName(controller.text);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_nodeNameUpdated)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_nodeNameUpdated),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
@@ -611,10 +630,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final interval = int.tryParse(intervalText);
|
||||
if (interval == null || interval < 60 || interval >= 86400) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.settings_locationIntervalInvalid),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_locationIntervalInvalid),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -622,8 +640,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await connector.setCustomVar("gps_interval:$interval");
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_locationUpdated)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_locationUpdated),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -643,15 +662,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
: currentLon;
|
||||
if (lat == null || lon == null) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_locationBothRequired)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_locationBothRequired),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_locationInvalid)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_locationInvalid),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -659,8 +680,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await connector.setNodeLocation(lat: lat, lon: lon);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_locationUpdated)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_locationUpdated),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
@@ -674,9 +696,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
void _syncTime(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
connector.syncTime();
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
|
||||
content: Text(l10n.settings_timeSynchronized),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
|
||||
@@ -741,23 +764,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (!mounted) return;
|
||||
switch (result) {
|
||||
case gpxExportSuccess:
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess)));
|
||||
content: Text(l10n.settings_gpxExportSuccess),
|
||||
);
|
||||
case gpxExportNoContacts:
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_gpxExportNoContacts)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_gpxExportNoContacts),
|
||||
);
|
||||
break;
|
||||
case gpxExportNotAvailable:
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_gpxExportNotAvailable),
|
||||
);
|
||||
break;
|
||||
case gpxExportFailed:
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError)));
|
||||
content: Text(l10n.settings_gpxExportError),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -984,6 +1011,15 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
title: Text(l10n.settings_multiAck),
|
||||
value: multiAcks == 1,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => multiAcks = value ? 1 : 0);
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: telemetryMode,
|
||||
decoration: InputDecoration(
|
||||
@@ -1025,21 +1061,6 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.settings_multiAck(multiAcks.toString()),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Slider(
|
||||
value: multiAcks.toDouble(),
|
||||
min: 0,
|
||||
max: 2,
|
||||
divisions: 2,
|
||||
label: multiAcks.toString(),
|
||||
onChanged: (value) {
|
||||
setDialogState(() => multiAcks = value.round());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1060,8 +1081,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_telemetryModeUpdated),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
@@ -1088,6 +1110,11 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
|
||||
final _txPowerController = TextEditingController(text: '20');
|
||||
bool _clientRepeat = false;
|
||||
int? _selectedPresetIndex;
|
||||
_RadioSettingsSnapshot? _lastNonRepeatSnapshot;
|
||||
|
||||
AppDebugLogService get _appLog =>
|
||||
Provider.of<AppDebugLogService>(context, listen: false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -1139,6 +1166,21 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
}
|
||||
|
||||
_clientRepeat = widget.connector.clientRepeat ?? false;
|
||||
_selectedPresetIndex = _findMatchingPresetIndex();
|
||||
if (_clientRepeat) {
|
||||
_lastNonRepeatSnapshot =
|
||||
_sessionRememberedNonRepeatSnapshot() ??
|
||||
_inferNonRepeatSnapshotForRepeatEnabled();
|
||||
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
|
||||
_lastNonRepeatSnapshot!,
|
||||
);
|
||||
} else {
|
||||
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_logRadioSettingsState('Dialog initialized');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1148,14 +1190,223 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _applyPreset(RadioSettings preset) {
|
||||
void _applyPreset(int index) {
|
||||
setState(() {
|
||||
_frequencyController.text = preset.frequencyMHz.toString();
|
||||
_bandwidth = preset.bandwidth;
|
||||
_spreadingFactor = preset.spreadingFactor;
|
||||
_codingRate = preset.codingRate;
|
||||
_txPowerController.text = preset.txPowerDbm.toString();
|
||||
_applyPresetState(index);
|
||||
});
|
||||
_logRadioSettingsState(
|
||||
'Applied preset ${RadioSettings.presets[index].$1} (#$index)',
|
||||
);
|
||||
}
|
||||
|
||||
int? _findMatchingPresetIndex() {
|
||||
return _findMatchingPresetIndexForSnapshot(_currentSnapshot());
|
||||
}
|
||||
|
||||
int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) {
|
||||
for (final i in _visiblePresetIndexes()) {
|
||||
final preset = RadioSettings.presets[i].$2;
|
||||
if (preset.frequencyHz == snapshot.frequencyHz &&
|
||||
preset.bandwidth == snapshot.bandwidth &&
|
||||
preset.spreadingFactor == snapshot.spreadingFactor &&
|
||||
preset.codingRate == snapshot.codingRate &&
|
||||
preset.txPowerDbm == snapshot.txPowerDbm) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Iterable<int> _visiblePresetIndexes() sync* {
|
||||
for (var i = 0; i < RadioSettings.presets.length; i++) {
|
||||
if (_isOffGridPresetIndex(i)) {
|
||||
continue;
|
||||
}
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
_RadioSettingsSnapshot _currentSnapshot() {
|
||||
final frequencyMHz = double.tryParse(_frequencyController.text) ?? 915.0;
|
||||
final txPowerDbm = int.tryParse(_txPowerController.text) ?? 20;
|
||||
return _RadioSettingsSnapshot(
|
||||
frequencyMHz: frequencyMHz,
|
||||
bandwidth: _bandwidth,
|
||||
spreadingFactor: _spreadingFactor,
|
||||
codingRate: _codingRate,
|
||||
txPowerDbm: txPowerDbm,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isOffGridPresetIndex(int? index) {
|
||||
if (index == null) return false;
|
||||
return RadioSettings.presets[index].$1.startsWith('Off-Grid ');
|
||||
}
|
||||
|
||||
double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) {
|
||||
if (baseFrequencyMHz < 500) return 433.0;
|
||||
if (baseFrequencyMHz < 900) return 869.0;
|
||||
return 918.0;
|
||||
}
|
||||
|
||||
double _normalFrequencyForBand(double frequencyMHz) {
|
||||
if (frequencyMHz < 500) return 433.650;
|
||||
if (frequencyMHz < 900) return 869.432;
|
||||
return 915.8;
|
||||
}
|
||||
|
||||
_RadioSettingsSnapshot _fallbackNonRepeatSnapshot(
|
||||
double currentFrequencyMHz,
|
||||
) {
|
||||
return _RadioSettingsSnapshot(
|
||||
frequencyMHz: _normalFrequencyForBand(currentFrequencyMHz),
|
||||
bandwidth: _bandwidth,
|
||||
spreadingFactor: _spreadingFactor,
|
||||
codingRate: _codingRate,
|
||||
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
|
||||
);
|
||||
}
|
||||
|
||||
_RadioSettingsSnapshot _nonRepeatSnapshotForCurrentSelection() {
|
||||
final current = _currentSnapshot();
|
||||
if (!_isOffGridPresetIndex(_selectedPresetIndex)) {
|
||||
return current;
|
||||
}
|
||||
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
|
||||
}
|
||||
|
||||
_RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() {
|
||||
final snapshot = widget.connector.rememberedNonRepeatRadioState;
|
||||
if (snapshot == null) return null;
|
||||
return _RadioSettingsSnapshot.fromMeshCoreSnapshot(snapshot);
|
||||
}
|
||||
|
||||
_RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() {
|
||||
final current = _currentSnapshot();
|
||||
for (final i in _visiblePresetIndexes()) {
|
||||
final preset = RadioSettings.presets[i].$2;
|
||||
final offGridFreqHz =
|
||||
(_offGridFrequencyForBaseFrequency(preset.frequencyMHz) * 1000)
|
||||
.round();
|
||||
if (offGridFreqHz == current.frequencyHz &&
|
||||
preset.bandwidth == current.bandwidth &&
|
||||
preset.spreadingFactor == current.spreadingFactor &&
|
||||
preset.codingRate == current.codingRate &&
|
||||
preset.txPowerDbm == current.txPowerDbm) {
|
||||
return _RadioSettingsSnapshot(
|
||||
frequencyMHz: preset.frequencyMHz,
|
||||
bandwidth: preset.bandwidth,
|
||||
spreadingFactor: preset.spreadingFactor,
|
||||
codingRate: preset.codingRate,
|
||||
txPowerDbm: preset.txPowerDbm,
|
||||
);
|
||||
}
|
||||
}
|
||||
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
|
||||
}
|
||||
|
||||
void _applySnapshot(_RadioSettingsSnapshot snapshot) {
|
||||
_frequencyController.text = snapshot.frequencyMHz.toStringAsFixed(3);
|
||||
_bandwidth = snapshot.bandwidth;
|
||||
_spreadingFactor = snapshot.spreadingFactor;
|
||||
_codingRate = snapshot.codingRate;
|
||||
_txPowerController.text = snapshot.txPowerDbm.toString();
|
||||
}
|
||||
|
||||
void _applyPresetState(int index) {
|
||||
final preset = RadioSettings.presets[index].$2;
|
||||
final baseSnapshot = _RadioSettingsSnapshot(
|
||||
frequencyMHz: preset.frequencyMHz,
|
||||
bandwidth: preset.bandwidth,
|
||||
spreadingFactor: preset.spreadingFactor,
|
||||
codingRate: preset.codingRate,
|
||||
txPowerDbm: preset.txPowerDbm,
|
||||
);
|
||||
final frequencyMHz = _clientRepeat
|
||||
? _offGridFrequencyForBaseFrequency(baseSnapshot.frequencyMHz)
|
||||
: baseSnapshot.frequencyMHz;
|
||||
_frequencyController.text = frequencyMHz.toString();
|
||||
_bandwidth = preset.bandwidth;
|
||||
_spreadingFactor = preset.spreadingFactor;
|
||||
_codingRate = preset.codingRate;
|
||||
_txPowerController.text = preset.txPowerDbm.toString();
|
||||
_selectedPresetIndex = index;
|
||||
_lastNonRepeatSnapshot = baseSnapshot;
|
||||
}
|
||||
|
||||
void _syncPresetSelection() {
|
||||
final previousPresetIndex = _selectedPresetIndex;
|
||||
final previousLastNonRepeat = _lastNonRepeatSnapshot;
|
||||
if (_clientRepeat) {
|
||||
final baseSnapshot =
|
||||
previousLastNonRepeat ?? _inferNonRepeatSnapshotForRepeatEnabled();
|
||||
if (_bandwidth != baseSnapshot.bandwidth ||
|
||||
_spreadingFactor != baseSnapshot.spreadingFactor ||
|
||||
_codingRate != baseSnapshot.codingRate ||
|
||||
(int.tryParse(_txPowerController.text) ?? 20) !=
|
||||
baseSnapshot.txPowerDbm) {
|
||||
_lastNonRepeatSnapshot = _RadioSettingsSnapshot(
|
||||
frequencyMHz: baseSnapshot.frequencyMHz,
|
||||
bandwidth: _bandwidth,
|
||||
spreadingFactor: _spreadingFactor,
|
||||
codingRate: _codingRate,
|
||||
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
|
||||
);
|
||||
}
|
||||
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
|
||||
_lastNonRepeatSnapshot ?? baseSnapshot,
|
||||
);
|
||||
if (previousPresetIndex != _selectedPresetIndex ||
|
||||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
|
||||
_logRadioSettingsState(
|
||||
'Preset match updated while repeat enabled: ${_presetLabel(previousPresetIndex)} -> ${_presetLabel(_selectedPresetIndex)}',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
|
||||
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
|
||||
_lastNonRepeatSnapshot!,
|
||||
);
|
||||
if (previousPresetIndex != _selectedPresetIndex ||
|
||||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
|
||||
_logRadioSettingsState(
|
||||
'Preset sync updated state from ${_presetLabel(previousPresetIndex)} to ${_presetLabel(_selectedPresetIndex)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleManualSettingsChanged(String source) {
|
||||
_logRadioSettingsState('Manual settings edit: $source');
|
||||
setState(_syncPresetSelection);
|
||||
}
|
||||
|
||||
void _handleClientRepeatChanged(bool enabled) {
|
||||
_logRadioSettingsState(
|
||||
'Off-grid repeat toggle requested: $_clientRepeat -> $enabled',
|
||||
);
|
||||
setState(() {
|
||||
final currentSnapshot = _currentSnapshot();
|
||||
if (enabled) {
|
||||
if (!_clientRepeat) {
|
||||
_syncPresetSelection();
|
||||
}
|
||||
final baseSnapshot = _lastNonRepeatSnapshot ?? currentSnapshot;
|
||||
_clientRepeat = true;
|
||||
_frequencyController.text = _offGridFrequencyForBaseFrequency(
|
||||
baseSnapshot.frequencyMHz,
|
||||
).toStringAsFixed(3);
|
||||
return;
|
||||
}
|
||||
|
||||
_clientRepeat = false;
|
||||
_applySnapshot(
|
||||
_lastNonRepeatSnapshot ??
|
||||
_fallbackNonRepeatSnapshot(currentSnapshot.frequencyMHz),
|
||||
);
|
||||
_syncPresetSelection();
|
||||
});
|
||||
_logRadioSettingsState('Off-grid repeat toggle applied');
|
||||
}
|
||||
|
||||
Future<void> _saveSettings() async {
|
||||
@@ -1164,18 +1415,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
final txPower = int.tryParse(_txPowerController.text);
|
||||
|
||||
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
|
||||
content: Text(l10n.settings_frequencyInvalid),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final maxTxPower = widget.connector.maxTxPower ?? 22;
|
||||
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1195,14 +1446,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
if (knownRepeat) {
|
||||
const validRepeatFreqsKHz = {433000, 869000, 918000};
|
||||
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_clientRepeatFreqWarning),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_logRadioSettingsState('Saving radio settings');
|
||||
await widget.connector.sendFrame(
|
||||
buildSetRadioParamsFrame(
|
||||
freqHz,
|
||||
@@ -1214,29 +1467,64 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
);
|
||||
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
|
||||
await widget.connector.refreshDeviceInfo();
|
||||
final rememberedSnapshot = _clientRepeat
|
||||
? _lastNonRepeatSnapshot
|
||||
: _currentSnapshot();
|
||||
if (rememberedSnapshot != null) {
|
||||
widget.connector.rememberNonRepeatRadioState(
|
||||
rememberedSnapshot.toMeshCoreSnapshot(widget.connector.currentCr),
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_radioSettingsUpdated)),
|
||||
_logRadioSettingsState('Radio settings saved successfully');
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_radioSettingsUpdated),
|
||||
);
|
||||
} catch (e) {
|
||||
_appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings');
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_error(e.toString()))),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_error(e.toString())),
|
||||
);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
int _toUiCodingRate(int deviceCr) {
|
||||
return deviceCr <= 4 ? deviceCr + 4 : deviceCr;
|
||||
}
|
||||
|
||||
int _toDeviceCodingRate(int uiCr, int? deviceCr) {
|
||||
if (deviceCr != null && deviceCr <= 4) {
|
||||
return uiCr - 4;
|
||||
String _presetLabel(int? index) {
|
||||
if (index == null) {
|
||||
return 'custom';
|
||||
}
|
||||
return uiCr;
|
||||
return '${RadioSettings.presets[index].$1} (#$index)';
|
||||
}
|
||||
|
||||
String _formatSnapshot(_RadioSettingsSnapshot? snapshot) {
|
||||
if (snapshot == null) {
|
||||
return 'null';
|
||||
}
|
||||
return '${snapshot.frequencyMHz.toStringAsFixed(3)}MHz/'
|
||||
'${snapshot.bandwidth.label}/'
|
||||
'${snapshot.spreadingFactor.label}/'
|
||||
'${snapshot.codingRate.label}/'
|
||||
'${snapshot.txPowerDbm}dBm';
|
||||
}
|
||||
|
||||
void _logRadioSettingsState(String message) {
|
||||
if (!kDebugMode) return;
|
||||
_appLog.info(
|
||||
'$message | '
|
||||
'freq=${_frequencyController.text}MHz '
|
||||
'bw=${_bandwidth.label} '
|
||||
'sf=${_spreadingFactor.label} '
|
||||
'cr=${_codingRate.label} '
|
||||
'tx=${_txPowerController.text}dBm '
|
||||
'repeat=$_clientRepeat '
|
||||
'preset=${_presetLabel(_selectedPresetIndex)} '
|
||||
'lastNonRepeat=${_formatSnapshot(_lastNonRepeatSnapshot)}',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1250,12 +1538,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<int>(
|
||||
key: ValueKey<int?>(_selectedPresetIndex),
|
||||
initialValue: _selectedPresetIndex,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_presets,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (var i = 0; i < RadioSettings.presets.length; i++)
|
||||
for (final i in _visiblePresetIndexes())
|
||||
DropdownMenuItem(
|
||||
value: i,
|
||||
child: Text(RadioSettings.presets[i].$1),
|
||||
@@ -1263,13 +1553,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
],
|
||||
onChanged: (index) {
|
||||
if (index != null) {
|
||||
_applyPreset(RadioSettings.presets[index].$2);
|
||||
_applyPreset(index);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _frequencyController,
|
||||
onChanged: (_) => _handleManualSettingsChanged('frequency'),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_frequency,
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -1292,7 +1583,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _bandwidth = value);
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_bandwidth = value;
|
||||
_syncPresetSelection();
|
||||
});
|
||||
_logRadioSettingsState('Manual settings edit: bandwidth');
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -1308,7 +1605,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _spreadingFactor = value);
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_spreadingFactor = value;
|
||||
_syncPresetSelection();
|
||||
});
|
||||
_logRadioSettingsState(
|
||||
'Manual settings edit: spreading factor',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -1324,12 +1629,19 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _codingRate = value);
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_codingRate = value;
|
||||
_syncPresetSelection();
|
||||
});
|
||||
_logRadioSettingsState('Manual settings edit: coding rate');
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _txPowerController,
|
||||
onChanged: (_) => _handleManualSettingsChanged('tx power'),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_txPower,
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -1345,7 +1657,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
title: Text(l10n.settings_clientRepeat),
|
||||
subtitle: Text(l10n.settings_clientRepeatSubtitle),
|
||||
value: _clientRepeat,
|
||||
onChanged: (value) => setState(() => _clientRepeat = value),
|
||||
onChanged: _handleClientRepeatChanged,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
@@ -1362,3 +1674,75 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioSettingsSnapshot {
|
||||
final double frequencyMHz;
|
||||
final LoRaBandwidth bandwidth;
|
||||
final LoRaSpreadingFactor spreadingFactor;
|
||||
final LoRaCodingRate codingRate;
|
||||
final int txPowerDbm;
|
||||
|
||||
const _RadioSettingsSnapshot({
|
||||
required this.frequencyMHz,
|
||||
required this.bandwidth,
|
||||
required this.spreadingFactor,
|
||||
required this.codingRate,
|
||||
required this.txPowerDbm,
|
||||
});
|
||||
|
||||
/// Frequency in integer Hz — avoids floating-point comparison issues.
|
||||
int get frequencyHz => (frequencyMHz * 1000).round();
|
||||
|
||||
/// Convert from the connector's raw-int snapshot to UI-enum snapshot.
|
||||
static _RadioSettingsSnapshot? fromMeshCoreSnapshot(
|
||||
MeshCoreRadioStateSnapshot snapshot,
|
||||
) {
|
||||
final bw = LoRaBandwidth.values
|
||||
.where((b) => b.hz == snapshot.bwHz)
|
||||
.firstOrNull;
|
||||
final sf = LoRaSpreadingFactor.values
|
||||
.where((s) => s.value == snapshot.sf)
|
||||
.firstOrNull;
|
||||
final cr = LoRaCodingRate.values
|
||||
.where((c) => c.value == _toUiCodingRate(snapshot.cr))
|
||||
.firstOrNull;
|
||||
if (bw == null || sf == null || cr == null) return null;
|
||||
return _RadioSettingsSnapshot(
|
||||
frequencyMHz: snapshot.freqHz / 1000.0,
|
||||
bandwidth: bw,
|
||||
spreadingFactor: sf,
|
||||
codingRate: cr,
|
||||
txPowerDbm: snapshot.txPowerDbm,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert back to the connector's raw-int snapshot.
|
||||
MeshCoreRadioStateSnapshot toMeshCoreSnapshot(int? deviceCr) {
|
||||
return MeshCoreRadioStateSnapshot(
|
||||
freqHz: frequencyHz,
|
||||
bwHz: bandwidth.hz,
|
||||
sf: spreadingFactor.value,
|
||||
cr: _toDeviceCodingRate(codingRate.value, deviceCr),
|
||||
txPowerDbm: txPowerDbm,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _RadioSettingsSnapshot &&
|
||||
frequencyHz == other.frequencyHz &&
|
||||
bandwidth == other.bandwidth &&
|
||||
spreadingFactor == other.spreadingFactor &&
|
||||
codingRate == other.codingRate &&
|
||||
txPowerDbm == other.txPowerDbm;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
frequencyHz,
|
||||
bandwidth,
|
||||
spreadingFactor,
|
||||
codingRate,
|
||||
txPowerDbm,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
@@ -270,8 +271,10 @@ class _TcpScreenState extends State<TcpScreen> {
|
||||
|
||||
void _showError(String message) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), backgroundColor: Colors.red),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../utils/app_logger.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
@@ -86,11 +87,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
_recordTelemetryResult(false);
|
||||
});
|
||||
@@ -137,11 +137,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
_parsedTelemetry = parsedTelemetry;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
@@ -182,11 +181,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
_isLoaded = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../utils/app_logger.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../utils/usb_port_labels.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'scanner_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
@@ -383,11 +384,10 @@ class _UsbScreenState extends State<UsbScreen> {
|
||||
|
||||
void _showError(Object error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_friendlyErrorMessage(error)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(_friendlyErrorMessage(error)),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../storage/prefs_manager.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
@@ -222,4 +223,34 @@ class AppSettingsService extends ChangeNotifier {
|
||||
Future<void> setJumpToOldestUnread(bool value) async {
|
||||
await updateSettings(_settings.copyWith(jumpToOldestUnread: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationEnabled(bool value) async {
|
||||
await updateSettings(_settings.copyWith(translationEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationTargetLanguageCode(String? value) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(translationTargetLanguageCode: value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setComposerTranslationEnabled(bool value) async {
|
||||
await updateSettings(_settings.copyWith(composerTranslationEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationModelSourceUrl(String? value) async {
|
||||
await updateSettings(_settings.copyWith(translationModelSourceUrl: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationSelectedModelId(String? value) async {
|
||||
await updateSettings(_settings.copyWith(translationSelectedModelId: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationDownloadedModels(
|
||||
List<TranslationModelRecord> value,
|
||||
) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(translationDownloadedModels: value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
|
||||
class BackgroundService {
|
||||
bool _initialized = false;
|
||||
String? Function()? _languageOverrideProvider;
|
||||
|
||||
/// Allows the app to expose its current language override (e.g. from
|
||||
/// AppSettingsService) so the foreground notification matches the app UI
|
||||
/// language instead of only the system locale.
|
||||
void setLanguageOverrideProvider(String? Function()? provider) {
|
||||
_languageOverrideProvider = provider;
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (!PlatformInfo.isAndroid || _initialized) return;
|
||||
@@ -34,13 +45,31 @@ class BackgroundService {
|
||||
}
|
||||
final running = await FlutterForegroundTask.isRunningService;
|
||||
if (running) return;
|
||||
final l10n = await _loadLocalizations();
|
||||
await FlutterForegroundTask.startService(
|
||||
notificationTitle: 'MeshCore running',
|
||||
notificationText: 'Keeping BLE connected',
|
||||
notificationTitle: l10n.background_serviceTitle,
|
||||
notificationText: l10n.background_serviceText,
|
||||
callback: startCallback,
|
||||
);
|
||||
}
|
||||
|
||||
Future<AppLocalizations> _loadLocalizations() async {
|
||||
final supported = AppLocalizations.supportedLocales;
|
||||
final override = _languageOverrideProvider?.call();
|
||||
if (override != null && override.isNotEmpty) {
|
||||
final overrideLocale = Locale(override);
|
||||
final isSupported = supported.any(
|
||||
(l) => l.languageCode == overrideLocale.languageCode,
|
||||
);
|
||||
if (isSupported) {
|
||||
return AppLocalizations.delegate.load(overrideLocale);
|
||||
}
|
||||
}
|
||||
final preferred = WidgetsBinding.instance.platformDispatcher.locales;
|
||||
final match = basicLocaleListResolution(preferred, supported);
|
||||
return AppLocalizations.delegate.load(match);
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
if (!PlatformInfo.isAndroid) return;
|
||||
final running = await FlutterForegroundTask.isRunningService;
|
||||
|
||||
@@ -24,6 +24,26 @@ class LineOfSightSample {
|
||||
});
|
||||
}
|
||||
|
||||
class LineOfSightObstruction {
|
||||
final int sampleIndex;
|
||||
final LatLng point;
|
||||
final double distanceMeters;
|
||||
final double clearanceMeters;
|
||||
final double obstructionMeters;
|
||||
final double terrainMeters;
|
||||
final double lineHeightMeters;
|
||||
|
||||
const LineOfSightObstruction({
|
||||
required this.sampleIndex,
|
||||
required this.point,
|
||||
required this.distanceMeters,
|
||||
required this.clearanceMeters,
|
||||
required this.obstructionMeters,
|
||||
required this.terrainMeters,
|
||||
required this.lineHeightMeters,
|
||||
});
|
||||
}
|
||||
|
||||
class LineOfSightResult {
|
||||
final bool hasData;
|
||||
final bool isClear;
|
||||
@@ -31,6 +51,7 @@ class LineOfSightResult {
|
||||
final double maxObstructionMeters;
|
||||
final double? firstObstructionDistanceMeters;
|
||||
final List<LineOfSightSample> samples;
|
||||
final List<LineOfSightObstruction> obstructions;
|
||||
final String? errorMessage;
|
||||
final double usedKFactor;
|
||||
final double? frequencyMHz;
|
||||
@@ -42,6 +63,7 @@ class LineOfSightResult {
|
||||
required this.maxObstructionMeters,
|
||||
required this.firstObstructionDistanceMeters,
|
||||
required this.samples,
|
||||
required this.obstructions,
|
||||
required this.usedKFactor,
|
||||
this.frequencyMHz,
|
||||
this.errorMessage,
|
||||
@@ -56,7 +78,8 @@ class LineOfSightResult {
|
||||
isClear = false,
|
||||
maxObstructionMeters = 0,
|
||||
firstObstructionDistanceMeters = null,
|
||||
samples = const [];
|
||||
samples = const [],
|
||||
obstructions = const [];
|
||||
}
|
||||
|
||||
class LineOfSightPathSegment {
|
||||
@@ -191,6 +214,7 @@ class LineOfSightService {
|
||||
maxObstructionMeters: 0,
|
||||
firstObstructionDistanceMeters: null,
|
||||
samples: const [],
|
||||
obstructions: const [],
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
@@ -249,7 +273,9 @@ class LineOfSightService {
|
||||
var maxObstructionMeters = 0.0;
|
||||
double? firstObstructionDistanceMeters;
|
||||
final samples = <LineOfSightSample>[];
|
||||
final obstructions = <LineOfSightObstruction>[];
|
||||
var isClear = true;
|
||||
LineOfSightObstruction? clusterWorstObstruction;
|
||||
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
|
||||
@@ -274,6 +300,23 @@ class LineOfSightService {
|
||||
maxObstructionMeters = obstruction;
|
||||
}
|
||||
firstObstructionDistanceMeters ??= distanceFromStart;
|
||||
final candidate = LineOfSightObstruction(
|
||||
sampleIndex: i,
|
||||
point: points[i],
|
||||
distanceMeters: distanceFromStart,
|
||||
clearanceMeters: clearance,
|
||||
obstructionMeters: obstruction,
|
||||
terrainMeters: terrainHeight,
|
||||
lineHeightMeters: lineHeight,
|
||||
);
|
||||
if (clusterWorstObstruction == null ||
|
||||
candidate.obstructionMeters >
|
||||
clusterWorstObstruction.obstructionMeters) {
|
||||
clusterWorstObstruction = candidate;
|
||||
}
|
||||
} else if (clusterWorstObstruction != null) {
|
||||
obstructions.add(clusterWorstObstruction);
|
||||
clusterWorstObstruction = null;
|
||||
}
|
||||
|
||||
samples.add(
|
||||
@@ -286,6 +329,9 @@ class LineOfSightService {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (clusterWorstObstruction != null) {
|
||||
obstructions.add(clusterWorstObstruction);
|
||||
}
|
||||
|
||||
return LineOfSightResult(
|
||||
hasData: true,
|
||||
@@ -294,6 +340,7 @@ class LineOfSightService {
|
||||
maxObstructionMeters: maxObstructionMeters,
|
||||
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
|
||||
samples: samples,
|
||||
obstructions: obstructions,
|
||||
usedKFactor: kFactor,
|
||||
frequencyMHz: frequencyMHz,
|
||||
);
|
||||
|
||||
@@ -138,6 +138,9 @@ class MessageRetryService extends ChangeNotifier {
|
||||
Future<void> sendMessageWithRetry({
|
||||
required Contact contact,
|
||||
required String text,
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
Uint8List? pathBytes,
|
||||
int? pathLength,
|
||||
}) async {
|
||||
@@ -150,6 +153,9 @@ class MessageRetryService extends ChangeNotifier {
|
||||
final message = Message(
|
||||
senderKey: contact.publicKey,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
timestamp: DateTime.now(),
|
||||
isOutgoing: true,
|
||||
status: MessageStatus.pending,
|
||||
|
||||
@@ -7,8 +7,42 @@ class StorageService {
|
||||
static const String _pathHistoryPrefix = 'path_history_';
|
||||
static const String _pendingMessagesKey = 'pending_messages';
|
||||
static const String _repeaterPasswordsKey = 'repeater_passwords';
|
||||
static const String _repeaterAutoClockSyncAfterLoginKey =
|
||||
'repeater_auto_clock_sync_after_login';
|
||||
static const String _deliveryObservationsKey = 'delivery_observations';
|
||||
|
||||
Future<Map<String, bool>> _loadRepeaterAutoClockSyncAfterLogin() async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey);
|
||||
|
||||
if (jsonStr == null) return {};
|
||||
|
||||
try {
|
||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
return json.map((key, value) => MapEntry(key, value == true));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
String repeaterPubKeyHex,
|
||||
) async {
|
||||
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
|
||||
return settings[repeaterPubKeyHex] ?? false;
|
||||
}
|
||||
|
||||
Future<void> setRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
String repeaterPubKeyHex,
|
||||
bool enabled,
|
||||
) async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
|
||||
settings[repeaterPubKeyHex] = enabled;
|
||||
final jsonStr = jsonEncode(settings);
|
||||
await prefs.setString(_repeaterAutoClockSyncAfterLoginKey, jsonStr);
|
||||
}
|
||||
|
||||
Future<void> savePathHistory(
|
||||
String contactPubKeyHex,
|
||||
ContactPathHistory history,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'translation_file_store_stub.dart'
|
||||
if (dart.library.io) 'translation_file_store_io.dart';
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../models/translation_support.dart';
|
||||
|
||||
class TranslationFileStore {
|
||||
Future<String> modelDirectoryPath() async {
|
||||
final baseDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${baseDir.path}/translation_models');
|
||||
if (!dir.existsSync()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
|
||||
final dir = Directory(await modelDirectoryPath());
|
||||
if (!dir.existsSync()) {
|
||||
return const [];
|
||||
}
|
||||
final models = <TranslationModelRecord>[];
|
||||
for (final entity in dir.listSync().whereType<File>()) {
|
||||
final name = entity.uri.pathSegments.last;
|
||||
// Skip hidden chunk files from interrupted parallel downloads.
|
||||
if (name.startsWith('.')) {
|
||||
await entity.delete();
|
||||
continue;
|
||||
}
|
||||
final stat = entity.statSync();
|
||||
models.add(
|
||||
TranslationModelRecord(
|
||||
id: name,
|
||||
name: name,
|
||||
sourceUrl: '',
|
||||
localPath: entity.path,
|
||||
downloadedAt: stat.modified,
|
||||
fileSizeBytes: stat.size,
|
||||
),
|
||||
);
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
Future<void> deleteModel(TranslationModelRecord model) async {
|
||||
await deleteFile(model.localPath);
|
||||
}
|
||||
|
||||
Future<void> deleteFile(String path) async {
|
||||
final file = File(path);
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<DownloadedModelFile> writeModelBytes({
|
||||
required String fileName,
|
||||
required Stream<List<int>> chunks,
|
||||
}) async {
|
||||
final directoryPath = await modelDirectoryPath();
|
||||
final file = File('$directoryPath/$fileName');
|
||||
final sink = file.openWrite();
|
||||
var fileSizeBytes = 0;
|
||||
var completed = false;
|
||||
try {
|
||||
await for (final chunk in chunks) {
|
||||
sink.add(chunk);
|
||||
fileSizeBytes += chunk.length;
|
||||
}
|
||||
completed = true;
|
||||
} finally {
|
||||
await sink.close();
|
||||
if (!completed && file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
return DownloadedModelFile(
|
||||
localPath: file.path,
|
||||
fileSizeBytes: fileSizeBytes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> chunkFilePath(String fileName, int index) async {
|
||||
final dir = await modelDirectoryPath();
|
||||
return '$dir/.${fileName}_chunk_$index';
|
||||
}
|
||||
|
||||
Future<DownloadedModelFile> combineChunks({
|
||||
required String fileName,
|
||||
required List<String> chunkPaths,
|
||||
}) async {
|
||||
final dir = await modelDirectoryPath();
|
||||
final finalPath = '$dir/$fileName';
|
||||
final sink = File(finalPath).openWrite();
|
||||
var totalSize = 0;
|
||||
var completed = false;
|
||||
try {
|
||||
for (final chunkPath in chunkPaths) {
|
||||
final chunkFile = File(chunkPath);
|
||||
await sink.addStream(chunkFile.openRead());
|
||||
totalSize += await chunkFile.length();
|
||||
}
|
||||
completed = true;
|
||||
} finally {
|
||||
await sink.close();
|
||||
for (final chunkPath in chunkPaths) {
|
||||
final file = File(chunkPath);
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
if (!completed) {
|
||||
final finalFile = File(finalPath);
|
||||
if (finalFile.existsSync()) {
|
||||
await finalFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
return DownloadedModelFile(localPath: finalPath, fileSizeBytes: totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadedModelFile {
|
||||
final String localPath;
|
||||
final int fileSizeBytes;
|
||||
|
||||
const DownloadedModelFile({
|
||||
required this.localPath,
|
||||
required this.fileSizeBytes,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import '../models/translation_support.dart';
|
||||
|
||||
class TranslationFileStore {
|
||||
Future<String> modelDirectoryPath() async {
|
||||
throw UnsupportedError('Local model storage is not supported on web.');
|
||||
}
|
||||
|
||||
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
Future<void> deleteModel(TranslationModelRecord model) async {}
|
||||
|
||||
Future<void> deleteFile(String path) async {}
|
||||
|
||||
Future<DownloadedModelFile> writeModelBytes({
|
||||
required String fileName,
|
||||
required Stream<List<int>> chunks,
|
||||
}) async {
|
||||
throw UnsupportedError('Local model downloads are not supported on web.');
|
||||
}
|
||||
|
||||
Future<String> chunkFilePath(String fileName, int index) async {
|
||||
throw UnsupportedError('Local model downloads are not supported on web.');
|
||||
}
|
||||
|
||||
Future<DownloadedModelFile> combineChunks({
|
||||
required String fileName,
|
||||
required List<String> chunkPaths,
|
||||
}) async {
|
||||
throw UnsupportedError('Local model downloads are not supported on web.');
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadedModelFile {
|
||||
final String localPath;
|
||||
final int fileSizeBytes;
|
||||
|
||||
const DownloadedModelFile({
|
||||
required this.localPath,
|
||||
required this.fileSizeBytes,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:llamadart/llamadart.dart';
|
||||
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'app_settings_service.dart';
|
||||
import 'translation_file_store.dart';
|
||||
|
||||
class TranslationResult {
|
||||
final String translatedText;
|
||||
final String targetLanguageCode;
|
||||
final String? detectedLanguageCode;
|
||||
final String? modelId;
|
||||
final MessageTranslationStatus status;
|
||||
|
||||
const TranslationResult({
|
||||
required this.translatedText,
|
||||
required this.targetLanguageCode,
|
||||
required this.status,
|
||||
this.detectedLanguageCode,
|
||||
this.modelId,
|
||||
});
|
||||
}
|
||||
|
||||
class TranslationDownloadCancelled implements Exception {
|
||||
const TranslationDownloadCancelled();
|
||||
|
||||
@override
|
||||
String toString() => 'Download canceled.';
|
||||
}
|
||||
|
||||
class TranslationService extends ChangeNotifier {
|
||||
final AppSettingsService _appSettingsService;
|
||||
final TranslationFileStore _fileStore;
|
||||
|
||||
TranslationService(
|
||||
this._appSettingsService, {
|
||||
TranslationFileStore? fileStore,
|
||||
}) : _fileStore = fileStore ?? TranslationFileStore();
|
||||
|
||||
bool _isBusy = false;
|
||||
bool _isDownloading = false;
|
||||
bool _cancelDownloadRequested = false;
|
||||
String? _lastError;
|
||||
Future<void> _queue = Future<void>.value();
|
||||
LlamaEngine? _engine;
|
||||
String? _loadedModelPath;
|
||||
String? _failedModelPath;
|
||||
int _downloadedBytes = 0;
|
||||
int? _downloadTotalBytes;
|
||||
String? _downloadFileName;
|
||||
|
||||
bool get isBusy => _isBusy;
|
||||
bool get isDownloading => _isDownloading;
|
||||
String? get lastError => _lastError;
|
||||
int get downloadedBytes => _downloadedBytes;
|
||||
int? get downloadTotalBytes => _downloadTotalBytes;
|
||||
String? get downloadFileName => _downloadFileName;
|
||||
double? get downloadProgress {
|
||||
final total = _downloadTotalBytes;
|
||||
if (!_isDownloading || total == null || total <= 0) {
|
||||
return null;
|
||||
}
|
||||
return (_downloadedBytes / total).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
AppSettings get _settings => _appSettingsService.settings;
|
||||
|
||||
String? resolvedTargetLanguageCode(String? fallbackLanguageCode) {
|
||||
return _settings.translationTargetLanguageCode ??
|
||||
_settings.languageOverride ??
|
||||
fallbackLanguageCode;
|
||||
}
|
||||
|
||||
String? resolvedIncomingLanguageCode(String? fallbackLanguageCode) {
|
||||
return _settings.translationTargetLanguageCode ??
|
||||
_settings.languageOverride ??
|
||||
fallbackLanguageCode ??
|
||||
'en';
|
||||
}
|
||||
|
||||
bool shouldTranslateIncoming({
|
||||
required String text,
|
||||
required bool isCli,
|
||||
required bool isOutgoing,
|
||||
}) {
|
||||
if (!_settings.translationEnabled || isCli || isOutgoing) {
|
||||
return false;
|
||||
}
|
||||
return _isPlainTextEligible(text);
|
||||
}
|
||||
|
||||
bool shouldTranslateOutgoing({
|
||||
required String text,
|
||||
required String? targetLanguageCode,
|
||||
}) {
|
||||
return _settings.composerTranslationEnabled &&
|
||||
targetLanguageCode != null &&
|
||||
targetLanguageCode.isNotEmpty &&
|
||||
_isPlainTextEligible(text);
|
||||
}
|
||||
|
||||
List<TranslationModelRecord> get availableModels =>
|
||||
_settings.translationDownloadedModels;
|
||||
|
||||
TranslationModelRecord? get selectedModel {
|
||||
final selectedId = _settings.translationSelectedModelId;
|
||||
if (selectedId == null) {
|
||||
return availableModels.isNotEmpty ? availableModels.first : null;
|
||||
}
|
||||
for (final model in availableModels) {
|
||||
if (model.id == selectedId) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return availableModels.isNotEmpty ? availableModels.first : null;
|
||||
}
|
||||
|
||||
Future<void> refreshDownloadedModels() async {
|
||||
if (_isDownloading) return;
|
||||
final scanned = await _fileStore.scanDownloadedModels();
|
||||
if (scanned.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final existingByPath = {
|
||||
for (final model in _settings.translationDownloadedModels)
|
||||
model.localPath: model,
|
||||
};
|
||||
final merged = scanned.map((model) {
|
||||
final existing = existingByPath[model.localPath];
|
||||
if (existing == null) {
|
||||
return model;
|
||||
}
|
||||
return TranslationModelRecord(
|
||||
id: existing.id,
|
||||
name: existing.name,
|
||||
sourceUrl: existing.sourceUrl,
|
||||
localPath: existing.localPath,
|
||||
downloadedAt: existing.downloadedAt,
|
||||
fileSizeBytes: model.fileSizeBytes,
|
||||
);
|
||||
}).toList();
|
||||
await _appSettingsService.setTranslationDownloadedModels(merged);
|
||||
_failedModelPath = null;
|
||||
if (_settings.translationSelectedModelId == null && merged.isNotEmpty) {
|
||||
await _appSettingsService.setTranslationSelectedModelId(merged.first.id);
|
||||
}
|
||||
}
|
||||
|
||||
static const int _parallelChunks = 8;
|
||||
static const int _parallelMinBytes = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
Future<TranslationModelRecord> downloadModel({
|
||||
required String sourceUrl,
|
||||
String? fileName,
|
||||
String? id,
|
||||
}) async {
|
||||
final uri = Uri.tryParse(sourceUrl);
|
||||
if (uri == null || !uri.hasScheme) {
|
||||
throw ArgumentError('Invalid model URL.');
|
||||
}
|
||||
return _runExclusive(() async {
|
||||
_setBusy(true);
|
||||
_setDownloading(true);
|
||||
_lastError = null;
|
||||
try {
|
||||
final resolvedFileName =
|
||||
fileName ??
|
||||
_sanitizeFileName(
|
||||
uri.pathSegments.isNotEmpty
|
||||
? uri.pathSegments.last
|
||||
: 'translation-model.gguf',
|
||||
);
|
||||
_downloadFileName = resolvedFileName;
|
||||
_downloadedBytes = 0;
|
||||
_cancelDownloadRequested = false;
|
||||
|
||||
// HEAD request to check size and range support.
|
||||
final headClient = http.Client();
|
||||
int? totalSize;
|
||||
bool supportsRange = false;
|
||||
try {
|
||||
final headResponse = await headClient.send(http.Request('HEAD', uri));
|
||||
totalSize = headResponse.contentLength;
|
||||
supportsRange =
|
||||
headResponse.headers['accept-ranges']?.contains('bytes') == true;
|
||||
await headResponse.stream.drain<void>();
|
||||
} finally {
|
||||
headClient.close();
|
||||
}
|
||||
|
||||
_downloadTotalBytes = totalSize;
|
||||
notifyListeners();
|
||||
|
||||
DownloadedModelFile downloaded;
|
||||
if (supportsRange &&
|
||||
totalSize != null &&
|
||||
totalSize > _parallelMinBytes) {
|
||||
downloaded = await _downloadParallel(
|
||||
uri: uri,
|
||||
fileName: resolvedFileName,
|
||||
totalSize: totalSize,
|
||||
);
|
||||
} else {
|
||||
downloaded = await _downloadSingle(
|
||||
uri: uri,
|
||||
fileName: resolvedFileName,
|
||||
);
|
||||
}
|
||||
|
||||
final record = TranslationModelRecord(
|
||||
id: id ?? resolvedFileName,
|
||||
name: resolvedFileName,
|
||||
sourceUrl: sourceUrl,
|
||||
localPath: downloaded.localPath,
|
||||
downloadedAt: DateTime.now(),
|
||||
fileSizeBytes: downloaded.fileSizeBytes,
|
||||
);
|
||||
final updated = [
|
||||
for (final existing in _settings.translationDownloadedModels)
|
||||
if (existing.id != record.id) existing,
|
||||
record,
|
||||
];
|
||||
await _appSettingsService.setTranslationDownloadedModels(updated);
|
||||
await _appSettingsService.setTranslationSelectedModelId(record.id);
|
||||
await _appSettingsService.setTranslationModelSourceUrl(sourceUrl);
|
||||
_failedModelPath = null;
|
||||
return record;
|
||||
} finally {
|
||||
_setDownloading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<DownloadedModelFile> _downloadSingle({
|
||||
required Uri uri,
|
||||
required String fileName,
|
||||
}) async {
|
||||
final client = http.Client();
|
||||
try {
|
||||
final response = await client.send(http.Request('GET', uri));
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw StateError('Model download failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
_downloadTotalBytes ??= response.contentLength;
|
||||
notifyListeners();
|
||||
final trackedStream = _trackDownloadProgress(response.stream);
|
||||
return await _fileStore.writeModelBytes(
|
||||
fileName: fileName,
|
||||
chunks: trackedStream,
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<DownloadedModelFile> _downloadParallel({
|
||||
required Uri uri,
|
||||
required String fileName,
|
||||
required int totalSize,
|
||||
}) async {
|
||||
final chunkSize = (totalSize / _parallelChunks).ceil();
|
||||
final chunkPaths = <String>[];
|
||||
final clients = <http.Client>[];
|
||||
var combineReached = false;
|
||||
try {
|
||||
final futures = <Future<void>>[];
|
||||
for (var i = 0; i < _parallelChunks; i++) {
|
||||
final start = i * chunkSize;
|
||||
final end = (start + chunkSize - 1).clamp(0, totalSize - 1);
|
||||
if (start >= totalSize) break;
|
||||
final chunkPath = await _fileStore.chunkFilePath(fileName, i);
|
||||
chunkPaths.add(chunkPath);
|
||||
final client = http.Client();
|
||||
clients.add(client);
|
||||
futures.add(
|
||||
_downloadRange(
|
||||
client: client,
|
||||
uri: uri,
|
||||
chunkPath: chunkPath,
|
||||
start: start,
|
||||
end: end,
|
||||
),
|
||||
);
|
||||
}
|
||||
await Future.wait(futures);
|
||||
if (_cancelDownloadRequested) {
|
||||
throw const TranslationDownloadCancelled();
|
||||
}
|
||||
_downloadFileName = 'Merging chunks...';
|
||||
notifyListeners();
|
||||
combineReached = true;
|
||||
return await _fileStore.combineChunks(
|
||||
fileName: fileName,
|
||||
chunkPaths: chunkPaths,
|
||||
);
|
||||
} finally {
|
||||
for (final client in clients) {
|
||||
client.close();
|
||||
}
|
||||
if (!combineReached) {
|
||||
for (final chunkPath in chunkPaths) {
|
||||
await _fileStore.deleteFile(chunkPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadRange({
|
||||
required http.Client client,
|
||||
required Uri uri,
|
||||
required String chunkPath,
|
||||
required int start,
|
||||
required int end,
|
||||
}) async {
|
||||
final request = http.Request('GET', uri);
|
||||
request.headers['Range'] = 'bytes=$start-$end';
|
||||
final response = await client.send(request);
|
||||
if (response.statusCode != 206) {
|
||||
await response.stream.drain<void>();
|
||||
throw StateError(
|
||||
'Range download failed: HTTP ${response.statusCode}'
|
||||
'${response.statusCode == 200 ? ' (server ignored Range header)' : ''}',
|
||||
);
|
||||
}
|
||||
final trackedStream = _trackDownloadProgress(response.stream);
|
||||
await _fileStore.writeModelBytes(
|
||||
fileName: chunkPath.split(RegExp(r'[/\\]')).last,
|
||||
chunks: trackedStream,
|
||||
);
|
||||
}
|
||||
|
||||
void cancelDownload() {
|
||||
if (!_isDownloading) {
|
||||
return;
|
||||
}
|
||||
_cancelDownloadRequested = true;
|
||||
_lastError = 'Download stopped.';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeModel(TranslationModelRecord model) async {
|
||||
await _runExclusive(() async {
|
||||
_setBusy(true);
|
||||
_lastError = null;
|
||||
await _fileStore.deleteModel(model);
|
||||
final updated = _settings.translationDownloadedModels
|
||||
.where((entry) => entry.id != model.id)
|
||||
.toList();
|
||||
await _appSettingsService.setTranslationDownloadedModels(updated);
|
||||
if (_settings.translationSelectedModelId == model.id) {
|
||||
await _appSettingsService.setTranslationSelectedModelId(
|
||||
updated.isNotEmpty ? updated.first.id : null,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<TranslationResult?> translateIncomingText({
|
||||
required String text,
|
||||
required String? targetLanguageCode,
|
||||
}) async {
|
||||
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
|
||||
return null;
|
||||
}
|
||||
final detectedLanguageCode = await detectLanguage(text);
|
||||
if (detectedLanguageCode != null &&
|
||||
detectedLanguageCode == targetLanguageCode) {
|
||||
return const TranslationResult(
|
||||
translatedText: '',
|
||||
targetLanguageCode: '',
|
||||
status: MessageTranslationStatus.skipped,
|
||||
);
|
||||
}
|
||||
final translatedText = await _translateText(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
sourceLanguageCode: detectedLanguageCode,
|
||||
);
|
||||
if (translatedText == null || translatedText.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
// If translation is nearly identical, text was already in target language.
|
||||
if (translatedText.trim().toLowerCase() == text.trim().toLowerCase()) {
|
||||
return const TranslationResult(
|
||||
translatedText: '',
|
||||
targetLanguageCode: '',
|
||||
status: MessageTranslationStatus.skipped,
|
||||
);
|
||||
}
|
||||
return TranslationResult(
|
||||
translatedText: translatedText.trim(),
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
detectedLanguageCode: detectedLanguageCode,
|
||||
modelId: selectedModel?.id,
|
||||
status: MessageTranslationStatus.completed,
|
||||
);
|
||||
}
|
||||
|
||||
Future<TranslationResult?> translateOutgoingText({
|
||||
required String text,
|
||||
required String? targetLanguageCode,
|
||||
}) async {
|
||||
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
|
||||
return null;
|
||||
}
|
||||
final detectedLanguageCode = await detectLanguage(text);
|
||||
if (detectedLanguageCode != null &&
|
||||
detectedLanguageCode == targetLanguageCode) {
|
||||
return const TranslationResult(
|
||||
translatedText: '',
|
||||
targetLanguageCode: '',
|
||||
status: MessageTranslationStatus.skipped,
|
||||
);
|
||||
}
|
||||
final translatedText = await _translateText(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
sourceLanguageCode: detectedLanguageCode,
|
||||
);
|
||||
if (translatedText == null || translatedText.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return TranslationResult(
|
||||
translatedText: translatedText.trim(),
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
detectedLanguageCode: detectedLanguageCode,
|
||||
modelId: selectedModel?.id,
|
||||
status: MessageTranslationStatus.completed,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> detectLanguage(String text) async {
|
||||
return _heuristicLanguageCode(text);
|
||||
}
|
||||
|
||||
Future<String?> _translateText({
|
||||
required String text,
|
||||
required String targetLanguageCode,
|
||||
String? sourceLanguageCode,
|
||||
}) async {
|
||||
if (!_hasUsableModel) {
|
||||
return null;
|
||||
}
|
||||
final model = selectedModel;
|
||||
if (model == null || model.localPath.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final targetLabel = _languageLabel(targetLanguageCode);
|
||||
final instruction = targetLanguageCode == 'zh'
|
||||
? '将以下文本翻译为中文,注意只需要输出翻译后的结果,不要额外解释:\n\n$text'
|
||||
: 'Translate the following segment into $targetLabel, without additional explanation.\n\n$text';
|
||||
try {
|
||||
return await _runExclusive(() async {
|
||||
final engine = await _ensureContext(model.localPath);
|
||||
if (engine == null) {
|
||||
return null;
|
||||
}
|
||||
final messages = [
|
||||
LlamaChatMessage.fromText(
|
||||
role: LlamaChatRole.user,
|
||||
text: instruction,
|
||||
),
|
||||
];
|
||||
final output = StringBuffer();
|
||||
await for (final chunk in engine.create(
|
||||
messages,
|
||||
params: const GenerationParams(
|
||||
maxTokens: 256,
|
||||
temp: 0.7,
|
||||
topK: 20,
|
||||
topP: 0.6,
|
||||
penalty: 1.05,
|
||||
reusePromptPrefix: false,
|
||||
),
|
||||
enableThinking: false,
|
||||
sourceLangCode: sourceLanguageCode,
|
||||
targetLangCode: targetLanguageCode,
|
||||
)) {
|
||||
final content = chunk.choices.firstOrNull?.delta.content;
|
||||
if (content != null) {
|
||||
output.write(content);
|
||||
}
|
||||
if (output.length >= text.length * 4 + 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return _sanitizeOutput(output.toString());
|
||||
});
|
||||
} catch (error) {
|
||||
_lastError = error.toString();
|
||||
appLogger.warn('Translation request failed: $error');
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get _hasUsableModel {
|
||||
final model = selectedModel;
|
||||
return !kIsWeb && model != null && model.localPath.isNotEmpty;
|
||||
}
|
||||
|
||||
bool _isPlainTextEligible(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (GifHelper.parseGif(trimmed) != null) {
|
||||
return false;
|
||||
}
|
||||
return !(trimmed.startsWith('m:') ||
|
||||
trimmed.startsWith('V1|') ||
|
||||
trimmed.startsWith('r:'));
|
||||
}
|
||||
|
||||
String? _heuristicLanguageCode(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
|
||||
return 'ja';
|
||||
}
|
||||
if (RegExp(r'[가-힣]').hasMatch(text)) {
|
||||
return 'ko';
|
||||
}
|
||||
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
final lower = trimmed.toLowerCase();
|
||||
final patterns = <String, String>{
|
||||
'uk': r'\b(привіт|дякую|будь|ласка|як|де|не|так|це|є|най|ще|може|для)\b',
|
||||
'ru':
|
||||
r'\b(что|это|как|не|да|нет|он|она|они|быть|есть|для|сегодня|если|уже|может)\b',
|
||||
'bg': r'\b(ще|няма|благодаря|моля|това|какво|тук|ние|вие|не|със|за)\b',
|
||||
'de':
|
||||
r'\b(der|die|das|und|ist|nicht|ein|eine|ich|für|mit|auf|zu|auch|als|an|im|am|es|dem|den|sich|von)\b',
|
||||
'en':
|
||||
r'\b(the|and|is|you|for|with|from|not|that|this|have|be|are|was|were|but|can|will|your|what|when|how|they)\b',
|
||||
'es':
|
||||
r'\b(el|la|los|las|es|que|de|en|con|por|para|no|un|una|se|como|su|al|del|está)\b',
|
||||
'fr':
|
||||
r'\b(le|la|les|un|une|et|est|que|qui|pour|dans|pas|avec|sur|ne|vous|il|elle|des|ce|cette|je|tu|nous|vous)\b',
|
||||
'it':
|
||||
r'\b(il|la|lo|un|una|che|di|da|in|per|con|non|si|mi|ti|noi|voi|lui|lei)\b',
|
||||
'pt':
|
||||
r'\b(os|as|que|de|do|da|em|para|com|por|não|uma|um|se|você|também)\b',
|
||||
'nl':
|
||||
r'\b(de|het|een|en|is|niet|dat|wat|je|ik|op|aan|voor|met|als|nog|zijn)\b',
|
||||
'sv':
|
||||
r'\b(och|är|det|att|som|en|på|inte|har|var|men|du|jag|vi|ni|den|detta)\b',
|
||||
'pl':
|
||||
r'\b(na|się|nie|jest|to|że|do|od|dla|czy|tak|ale|ma|jak|on|ona|my)\b',
|
||||
'sk': r'\b(je|na|so|že|do|od|za|si|to|ten|tá|tí|ako|má|nie|som|sa)\b',
|
||||
'sl': r'\b(in|je|na|se|da|za|od|ne|to|ta|so|kako|bo|sem|si)\b',
|
||||
'hu':
|
||||
r'\b(az|és|nem|van|volt|hogy|mit|mire|ki|mi|ez|azért|is|de|ha|te|ő|mi|itt)\b',
|
||||
};
|
||||
|
||||
final scores = <String, int>{};
|
||||
for (final entry in patterns.entries) {
|
||||
scores[entry.key] = RegExp(
|
||||
entry.value,
|
||||
caseSensitive: false,
|
||||
).allMatches(lower).length;
|
||||
}
|
||||
|
||||
final sorted = scores.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
if (sorted.isEmpty || sorted.first.value == 0) {
|
||||
return null;
|
||||
}
|
||||
if (sorted.length > 1 && sorted.first.value == sorted[1].value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sorted.first.key;
|
||||
}
|
||||
|
||||
String _languageLabel(String code) {
|
||||
for (final option in supportedTranslationLanguages) {
|
||||
if (option.code == code) {
|
||||
return option.label;
|
||||
}
|
||||
}
|
||||
return code.toUpperCase();
|
||||
}
|
||||
|
||||
String _sanitizeOutput(String raw) {
|
||||
var result = raw.trim();
|
||||
result = result.replaceAll(RegExp(r'\*\*'), '');
|
||||
result = result.replaceAll(RegExp(r'<[^>]+>'), '');
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
String _sanitizeFileName(String fileName) {
|
||||
final cleaned = fileName.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
|
||||
return cleaned.isEmpty ? 'translation-model.gguf' : cleaned;
|
||||
}
|
||||
|
||||
Future<LlamaEngine?> _ensureContext(String modelPath) async {
|
||||
if (_engine != null && _loadedModelPath == modelPath) {
|
||||
return _engine;
|
||||
}
|
||||
if (modelPath == _failedModelPath) {
|
||||
return null;
|
||||
}
|
||||
if (_engine != null) {
|
||||
await _engine!.dispose();
|
||||
_engine = null;
|
||||
_loadedModelPath = null;
|
||||
}
|
||||
final engine = LlamaEngine(LlamaBackend());
|
||||
try {
|
||||
await engine.loadModel(
|
||||
modelPath,
|
||||
modelParams: const ModelParams(
|
||||
gpuLayers: 0,
|
||||
preferredBackend: GpuBackend.cpu,
|
||||
),
|
||||
);
|
||||
_engine = engine;
|
||||
_loadedModelPath = modelPath;
|
||||
_failedModelPath = null;
|
||||
return _engine;
|
||||
} catch (_) {
|
||||
await engine.dispose();
|
||||
_failedModelPath = modelPath;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> releaseModel() async {
|
||||
await _runExclusive(() async {
|
||||
final engine = _engine;
|
||||
if (engine == null) {
|
||||
_loadedModelPath = null;
|
||||
return;
|
||||
}
|
||||
_engine = null;
|
||||
_loadedModelPath = null;
|
||||
await engine.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
Future<T> _runExclusive<T>(Future<T> Function() action) {
|
||||
final completer = Completer<T>();
|
||||
_setBusy(true);
|
||||
_queue = _queue.then((_) async {
|
||||
try {
|
||||
completer.complete(await action());
|
||||
} catch (error, stackTrace) {
|
||||
completer.completeError(error, stackTrace);
|
||||
} finally {
|
||||
_setBusy(false);
|
||||
}
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Stream<List<int>> _trackDownloadProgress(Stream<List<int>> source) async* {
|
||||
await for (final chunk in source) {
|
||||
if (_cancelDownloadRequested) {
|
||||
throw const TranslationDownloadCancelled();
|
||||
}
|
||||
_downloadedBytes += chunk.length;
|
||||
notifyListeners();
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
|
||||
void _setBusy(bool value) {
|
||||
if (_isBusy == value) {
|
||||
return;
|
||||
}
|
||||
_isBusy = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setDownloading(bool value) {
|
||||
_isDownloading = value;
|
||||
if (!value) {
|
||||
_cancelDownloadRequested = false;
|
||||
_downloadedBytes = 0;
|
||||
_downloadTotalBytes = null;
|
||||
_downloadFileName = null;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final engine = _engine;
|
||||
_engine = null;
|
||||
_loadedModelPath = null;
|
||||
if (engine != null) {
|
||||
unawaited(engine.dispose());
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:typed_data';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
@@ -98,6 +99,11 @@ class ChannelMessageStore {
|
||||
'senderKey': msg.senderKey != null ? base64Encode(msg.senderKey!) : null,
|
||||
'senderName': msg.senderName,
|
||||
'text': msg.text,
|
||||
'originalText': msg.originalText,
|
||||
'translatedText': msg.translatedText,
|
||||
'translatedLanguageCode': msg.translatedLanguageCode,
|
||||
'translationStatus': msg.translationStatus.value,
|
||||
'translationModelId': msg.translationModelId,
|
||||
'timestamp': msg.timestamp.millisecondsSinceEpoch,
|
||||
'isOutgoing': msg.isOutgoing,
|
||||
'status': msg.status.index,
|
||||
@@ -126,6 +132,13 @@ class ChannelMessageStore {
|
||||
: null,
|
||||
senderName: json['senderName'] as String,
|
||||
text: decodedText,
|
||||
originalText: json['originalText'] as String?,
|
||||
translatedText: json['translatedText'] as String?,
|
||||
translatedLanguageCode: json['translatedLanguageCode'] as String?,
|
||||
translationStatus: parseMessageTranslationStatus(
|
||||
json['translationStatus'],
|
||||
),
|
||||
translationModelId: json['translationModelId'] as String?,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
|
||||
isOutgoing: json['isOutgoing'] as bool,
|
||||
status: ChannelMessageStatus.values[json['status'] as int],
|
||||
|
||||
@@ -43,6 +43,7 @@ class ContactDiscoveryStore {
|
||||
'latitude': contact.latitude,
|
||||
'longitude': contact.longitude,
|
||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||
'lastModified': contact.lastModified?.millisecondsSinceEpoch,
|
||||
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||
'rawPacket': contact.rawPacket != null
|
||||
? base64Encode(contact.rawPacket!)
|
||||
@@ -53,6 +54,7 @@ class ContactDiscoveryStore {
|
||||
Contact _fromJson(Map<String, dynamic> json) {
|
||||
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
||||
final lastMessageMs = json['lastMessageAt'] as int?;
|
||||
final lastModifiedMs = json['lastModified'] as int?;
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
@@ -71,6 +73,9 @@ class ContactDiscoveryStore {
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||
lastModified: lastModifiedMs == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(lastModifiedMs),
|
||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastMessageMs ?? lastSeenMs,
|
||||
),
|
||||
|
||||
@@ -75,6 +75,7 @@ class ContactStore {
|
||||
'latitude': contact.latitude,
|
||||
'longitude': contact.longitude,
|
||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||
'lastModified': contact.lastModified?.millisecondsSinceEpoch,
|
||||
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||
'isActive': contact.isActive,
|
||||
'rawPacket': contact.rawPacket != null
|
||||
@@ -86,6 +87,7 @@ class ContactStore {
|
||||
Contact _fromJson(Map<String, dynamic> json) {
|
||||
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
||||
final lastMessageMs = json['lastMessageAt'] as int?;
|
||||
final lastModifiedMs = json['lastModified'] as int?;
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
@@ -104,6 +106,9 @@ class ContactStore {
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||
lastModified: lastModifiedMs == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(lastModifiedMs),
|
||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastMessageMs ?? lastSeenMs,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import '../models/message.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
@@ -83,6 +84,11 @@ class MessageStore {
|
||||
'isCli': msg.isCli,
|
||||
'status': msg.status.index,
|
||||
'messageId': msg.messageId,
|
||||
'originalText': msg.originalText,
|
||||
'translatedText': msg.translatedText,
|
||||
'translatedLanguageCode': msg.translatedLanguageCode,
|
||||
'translationStatus': msg.translationStatus.value,
|
||||
'translationModelId': msg.translationModelId,
|
||||
'retryCount': msg.retryCount,
|
||||
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
|
||||
'expectedAckHash': msg.expectedAckHash,
|
||||
@@ -115,6 +121,13 @@ class MessageStore {
|
||||
isCli: isCli,
|
||||
status: MessageStatus.values[json['status'] as int],
|
||||
messageId: json['messageId'] as String?,
|
||||
originalText: json['originalText'] as String?,
|
||||
translatedText: json['translatedText'] as String?,
|
||||
translatedLanguageCode: json['translatedLanguageCode'] as String?,
|
||||
translationStatus: parseMessageTranslationStatus(
|
||||
json['translationStatus'],
|
||||
),
|
||||
translationModelId: json['translationModelId'] as String?,
|
||||
retryCount: json['retryCount'] as int? ?? 0,
|
||||
estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?,
|
||||
expectedAckHash: json['expectedAckHash'] as int? ?? 0,
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// MeshCore redesign palette — warm field-journal dark theme with
|
||||
/// phosphor-green signal accents. Mirrors values from the redesign spec.
|
||||
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);
|
||||
|
||||
// Lines
|
||||
static const line = Color(0xFF232C28);
|
||||
static const line2 = Color(0xFF34403A);
|
||||
static const line3 = Color(0xFF48564F);
|
||||
|
||||
// Ink
|
||||
static const ink = Color(0xFFEFF3E8);
|
||||
static const ink2 = Color(0xFFBAC4B5);
|
||||
static const ink3 = Color(0xFF7C8B82);
|
||||
static const ink4 = Color(0xFF55635B);
|
||||
|
||||
// Signal (phosphor)
|
||||
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);
|
||||
static const warnDim = Color(0xFFC27E3C);
|
||||
static const warnBg = Color(0x1CFFA552);
|
||||
static const warnLine = Color(0x4DFFA552);
|
||||
|
||||
// Alert (coral)
|
||||
static const alert = Color(0xFFFF6A5C);
|
||||
static const alertBg = Color(0x1CFF6A5C);
|
||||
static const alertLine = Color(0x52FF6A5C);
|
||||
|
||||
// Blue (dusk sky)
|
||||
static const blue = Color(0xFF7FCBF5);
|
||||
static const blueBg = Color(0x1C7FCBF5);
|
||||
static const blueLine = Color(0x477FCBF5);
|
||||
|
||||
// Magenta
|
||||
static const magenta = Color(0xFFDE7FDB);
|
||||
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);
|
||||
|
||||
// ── 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);
|
||||
}
|
||||
|
||||
/// Named font stacks — Flutter falls back to system fonts when the named
|
||||
/// family isn't installed, keeping things working without bundled assets.
|
||||
class MeshFonts {
|
||||
MeshFonts._();
|
||||
|
||||
static const sans = 'Inter';
|
||||
static const mono = 'JetBrains Mono';
|
||||
static const display = 'Instrument Serif';
|
||||
|
||||
static const List<String> sansFallback = [
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'Roboto',
|
||||
'Noto Sans',
|
||||
'sans-serif',
|
||||
];
|
||||
static const List<String> monoFallback = [
|
||||
'SF Mono',
|
||||
'Menlo',
|
||||
'Consolas',
|
||||
'Roboto Mono',
|
||||
'monospace',
|
||||
];
|
||||
static const List<String> displayFallback = [
|
||||
'Cormorant Garamond',
|
||||
'Georgia',
|
||||
'Times New Roman',
|
||||
'serif',
|
||||
];
|
||||
}
|
||||
|
||||
/// Radii used consistently across the app.
|
||||
class MeshRadii {
|
||||
MeshRadii._();
|
||||
static const xs = 6.0;
|
||||
static const sm = 10.0;
|
||||
static const md = 14.0;
|
||||
static const lg = 18.0;
|
||||
static const xl = 24.0;
|
||||
static const pill = 999.0;
|
||||
}
|
||||
|
||||
/// Shared helpers exposed via [MeshTheme.of].
|
||||
class MeshTheme {
|
||||
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),
|
||||
error: MeshPalette.alert,
|
||||
onError: Color(0xFF1A0A08),
|
||||
errorContainer: MeshPalette.alertBg,
|
||||
onErrorContainer: MeshPalette.alert,
|
||||
surface: MeshPalette.bg,
|
||||
onSurface: MeshPalette.ink,
|
||||
surfaceContainerLowest: MeshPalette.bg,
|
||||
surfaceContainerLow: MeshPalette.bg1,
|
||||
surfaceContainer: MeshPalette.bg1,
|
||||
surfaceContainerHigh: MeshPalette.bg2,
|
||||
surfaceContainerHighest: MeshPalette.bg3,
|
||||
onSurfaceVariant: MeshPalette.ink2,
|
||||
outline: MeshPalette.line2,
|
||||
outlineVariant: MeshPalette.line,
|
||||
shadow: Colors.black,
|
||||
scrim: Colors.black54,
|
||||
inverseSurface: MeshPalette.ink,
|
||||
onInverseSurface: MeshPalette.bg,
|
||||
inversePrimary: MeshPalette.signalDim,
|
||||
);
|
||||
return _build(scheme, Brightness.dark);
|
||||
}
|
||||
|
||||
static ThemeData light() {
|
||||
const scheme = ColorScheme.light(
|
||||
primary: MeshPalette.lightSignal,
|
||||
onPrimary: Colors.white,
|
||||
primaryContainer: Color(0xFFD4E8D8),
|
||||
onPrimaryContainer: MeshPalette.lightSignal,
|
||||
secondary: Color(0xFF2F6EA8),
|
||||
onSecondary: Colors.white,
|
||||
tertiary: Color(0xFF8C4A8A),
|
||||
onTertiary: Colors.white,
|
||||
error: Color(0xFFB53D2F),
|
||||
onError: Colors.white,
|
||||
surface: MeshPalette.lightBg,
|
||||
onSurface: MeshPalette.lightInk,
|
||||
surfaceContainerLowest: MeshPalette.lightBg,
|
||||
surfaceContainerLow: MeshPalette.lightBg1,
|
||||
surfaceContainer: MeshPalette.lightBg1,
|
||||
surfaceContainerHigh: MeshPalette.lightBg2,
|
||||
surfaceContainerHighest: Color(0xFFD5D0C0),
|
||||
onSurfaceVariant: MeshPalette.lightInk2,
|
||||
outline: MeshPalette.lightLine,
|
||||
outlineVariant: Color(0xFFDBD6C6),
|
||||
);
|
||||
return _build(scheme, Brightness.light);
|
||||
}
|
||||
|
||||
static ThemeData _build(ColorScheme scheme, Brightness brightness) {
|
||||
final baseText =
|
||||
Typography.material2021(
|
||||
platform: TargetPlatform.android,
|
||||
colorScheme: scheme,
|
||||
).black.apply(
|
||||
bodyColor: scheme.onSurface,
|
||||
displayColor: scheme.onSurface,
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: brightness,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
canvasColor: scheme.surface,
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
textTheme: baseText,
|
||||
dividerColor: scheme.outlineVariant,
|
||||
dividerTheme: DividerThemeData(
|
||||
color: scheme.outlineVariant,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: scheme.surface,
|
||||
foregroundColor: scheme.onSurface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.2,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
iconTheme: IconThemeData(color: scheme.onSurface),
|
||||
shape: Border(
|
||||
bottom: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||
),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
side: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 0),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
iconColor: scheme.onSurfaceVariant,
|
||||
textColor: scheme.onSurface,
|
||||
tileColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
hoverElevation: 0,
|
||||
highlightElevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
extendedTextStyle: const TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: scheme.onSurface,
|
||||
side: BorderSide(color: scheme.outline),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: scheme.primary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: scheme.surfaceContainerHigh,
|
||||
hintStyle: TextStyle(color: scheme.onSurfaceVariant),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.outline),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.primary, width: 1.5),
|
||||
),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
side: BorderSide(color: scheme.outlineVariant),
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: MeshFonts.sans,
|
||||
fontFamilyFallback: MeshFonts.sansFallback,
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
indicatorColor: scheme.primary.withValues(alpha: 0.14),
|
||||
indicatorShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.mono,
|
||||
fontFamilyFallback: MeshFonts.monoFallback,
|
||||
fontSize: 10,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
letterSpacing: 0.1,
|
||||
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||
);
|
||||
}),
|
||||
iconTheme: WidgetStateProperty.resolveWith((states) {
|
||||
final selected = states.contains(WidgetState.selected);
|
||||
return IconThemeData(
|
||||
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||
size: 22,
|
||||
);
|
||||
}),
|
||||
),
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
modalBackgroundColor: scheme.surfaceContainerLow,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(MeshRadii.lg),
|
||||
),
|
||||
),
|
||||
),
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: scheme.surfaceContainerLow,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.lg),
|
||||
),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: scheme.surfaceContainerHigh,
|
||||
contentTextStyle: TextStyle(color: scheme.onSurface),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
),
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
color: scheme.surfaceContainerHigh,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
),
|
||||
iconTheme: IconThemeData(color: scheme.onSurfaceVariant, size: 22),
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
);
|
||||
}
|
||||
|
||||
/// Mono text style — sizes default to the body size Inter is using.
|
||||
static TextStyle mono({
|
||||
double? fontSize,
|
||||
FontWeight? fontWeight,
|
||||
Color? color,
|
||||
double? letterSpacing,
|
||||
}) {
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.mono,
|
||||
fontFamilyFallback: MeshFonts.monoFallback,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
letterSpacing: letterSpacing ?? 0.2,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
);
|
||||
}
|
||||
|
||||
/// Serif display style.
|
||||
static TextStyle display({
|
||||
double? fontSize,
|
||||
FontWeight? fontWeight,
|
||||
Color? color,
|
||||
double? letterSpacing,
|
||||
}) {
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.display,
|
||||
fontFamilyFallback: MeshFonts.displayFallback,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight ?? FontWeight.w400,
|
||||
color: color,
|
||||
letterSpacing: letterSpacing ?? -0.2,
|
||||
);
|
||||
}
|
||||
|
||||
/// Small-caps mono label — used for section accents and chip labels.
|
||||
static TextStyle accentLabel({Color? color, double? fontSize}) {
|
||||
return TextStyle(
|
||||
fontFamily: MeshFonts.mono,
|
||||
fontFamilyFallback: MeshFonts.monoFallback,
|
||||
fontSize: fontSize ?? 9.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.8,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Color-code an SNR value for consistency across the app.
|
||||
static Color snrColor(num? snr, {required bool blocked}) {
|
||||
if (blocked) return MeshPalette.alert;
|
||||
if (snr == null) return MeshPalette.ink3;
|
||||
if (snr > -5) return MeshPalette.signal;
|
||||
if (snr > -12) return MeshPalette.warn;
|
||||
return MeshPalette.alert;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user