Compare commits

..

359 Commits

Author SHA1 Message Date
Enot (ded) Skelly 00636c9084 rename ble debug log
companion protocol so changed to companion and ref:
BLE/TCP/USB
2026-05-05 15:25:32 -07:00
zjs81 67238468ce Merge pull request #423 from zjs81/offgrid-CR
increase CR for off grid
2026-05-05 15:14:00 -07:00
Enot (ded) Skelly ae32e76563 fix someones formatting 2026-05-05 11:07:32 -07:00
Enot (ded) Skelly 5572c9ee75 increase CR for off grid
default should be higher assuming trees etc
2026-05-05 10:59:36 -07:00
zjs81 f1d93bd5e8 Merge pull request #399 from zjs81/contacts-sync
fix issues with contact sync
2026-04-30 15:41:25 -07:00
zjs81 eb597b6c68 Merge pull request #416 from zjs81/dev-DesktopMapControls
Add desktop map controls
2026-04-29 12:32:36 -07:00
zjs81 efe21c4e87 Merge pull request #417 from ericszimmermann/ez_latin_heuristics2
latin languages heuristics
2026-04-29 12:31:23 -07:00
ZIER 38fece3313 replace pattern with String. 2026-04-29 11:51:50 +02:00
ZIER 3af3cce606 latin languages heuristics 2026-04-29 11:04:36 +02:00
Ded 026ec6f7de bump app protocol version as we support v4+ features (#398) 2026-04-28 22:35:48 -07:00
Winston Lowe eb50249b93 Add desktop map controls and improve zoom functionality across multiple screens 2026-04-28 19:26:51 -07:00
zjs81 99c0ab7e22 Merge pull request #404 from pioneer/ukrainian-translations
Ukrainian translation polished + localized hardcoded strings
2026-04-27 13:24:43 -07:00
zjs81 2950a9a687 Merge branch 'dev' into pr-404-merge 2026-04-27 13:23:53 -07:00
zjs81 1b3de54873 Merge pull request #412 from just-stuff-tm/enhancement/los-obstruction-pinning-411
add selectable LOS obstruction pinning for repeater placement Enhancement #411
2026-04-27 13:13:40 -07:00
zjs81 20a9ef3c2b Merge branch 'dev' into enhancement/los-obstruction-pinning-411 2026-04-27 13:13:19 -07:00
zjs81 a741e12ad1 Merge pull request #413 from ericszimmermann/ez_marker_update_squashed
Improve SharedMarker handling
2026-04-27 13:12:09 -07:00
zjs81 e54f30d6fb Merge pull request #414 from Diadlo/fix/jump_to_unread
Improve work with unread messages
2026-04-27 13:11:00 -07:00
zjs81 e1d23ad2c7 style: dart format
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:09:10 -07:00
zjs81 f07993b367 fix(chat): remove unnecessary Navigator.pop calls after setting unread counts 2026-04-27 13:07:21 -07:00
zjs81 0e5f1a45c4 fix(chat): address mark-as-unread double-pop and missed map entry point
- Remove stray Navigator.pop(context) in _markAsUnread for both contact
  and channel chats so the action no longer exits the conversation
- Thread initialUnreadCount through map discovered-contact "Open Chat"
  button so the unread divider/jump still fires from that entry point

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:07:16 -07:00
Dmitry Polshakov f10aeaeba8 chore(l10n): regenerate localizations for mark-as-unread strings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 15:58:03 +03:00
Dmitry Polshakov 00e4f52d75 feat(chat): add "Mark as Unread" action and unread messages divider
- Add "Mark as Unread" option to message context menu in both
  contact and channel chats
- Show "New messages" divider line between read and unread messages
- Add setContactUnreadCount/setChannelUnreadCount methods to connector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 15:58:03 +03:00
Dmitry Polshakov 3ea2e4763e fix(chat): fix jump-to-oldest-unread scroll not reaching target message
- Pass initialUnreadCount to chat screens before markRead clears it
- Use two-phase scroll: jumpTo estimated offset to build lazy items,
  then ensureVisible for precise positioning
- Await ensureVisible before clearing scroll guard to prevent
  scrollToBottomIfAtBottom from overriding the animation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 14:07:02 +03:00
ericz 94d9afe8b1 consolidate parsing in single parseMarkerText in map_screen.dart 2026-04-26 01:23:33 +02:00
ericz 7db3a12723 squashed commit for: deduplicate markers, allow for updates on position with same label with drawing line, get marker back after deletion in map through tabbing on icon in poi-message. 2026-04-26 00:13:26 +02:00
just-stuff-tm fcf10b4a73 added strings translategemma didnt translate to proper locallization 2026-04-25 09:11:49 -04:00
just-stuff-tm 7f353490cf contextstream/ is added to .gitignore 2026-04-25 09:07:28 -04:00
just_stuff_tm 46683e0ec2 Delete .contextstream/config.json 2026-04-25 09:04:00 -04:00
just-stuff-tm 4e368d562d add selectable LOS obstruction pinning for repeater placement 2026-04-25 08:56:28 -04:00
Serge Tarkovski 92d3009eb4 Fix swapped url/desc args in GPX export and add ContactLocalization unit tests 2026-04-25 01:32:43 +03:00
Serge Tarkovski f8d00caae0 Validate locale override and use preferred locale list for fallback 2026-04-25 01:03:11 +03:00
Serge Tarkovski e03d80b71f Merge remote-tracking branch 'origin/dev' into ukrainian-translations 2026-04-25 00:48:41 +03:00
Serge Tarkovski b7d0db8d1c Refactor: move Contact UI labels to l10n extension; rename raw getter to typeLabelRaw 2026-04-25 00:29:20 +03:00
Serge Tarkovski 6ae3f612ae Localize Score, fix login dialog overflow, use locale-aware date format in channel chat 2026-04-24 13:58:31 +03:00
zjs81 40d3941aab Merge pull request #405 from zjs81/#401-make-multi-ack-a-toggle
#401 make multi ack a toggle
2026-04-23 23:54:52 -07:00
Zach e53c493e78 update TS 2026-04-23 18:01:35 -07:00
Zach 54e0dae172 Add placeholder for multi-ACKs setting in localization 2026-04-23 17:58:40 -07:00
Zach 066aba7c5d #401 Refactor multi-ACK localization strings and settings UI
- Updated localization files for multiple languages to change the representation of multi-ACK settings from a string with a placeholder to a simple string.
- Removed unnecessary placeholder definitions for multi-ACK in localization files.
- Adjusted the settings screen to replace the slider for multi-ACK with a switch, simplifying the user interface.
- Updated the Podfile.lock to remove the wakelock_plus dependency.
2026-04-23 17:58:15 -07:00
Serge Tarkovski 5e446207c6 Ukrainian translation polished; localized remaining hardcoded UI strings 2026-04-23 17:47:37 +03:00
Enot (ded) Skelly 820bac0db0 fix issues with contact sync
this adds the actual last modified timestamp when present, before we used
last advert time as last modified in error

also sets _pendingInitialContactsSync to true on first connect over BLE
2026-04-21 16:44:04 -07:00
zjs81 6b6a881c7a Merge pull request #388 from zjs81/msg-chars
add byte counted text input
2026-04-20 09:17:00 -07:00
ericz 8ef8a38495 change to prepare Outbound Text Functions. 2026-04-17 18:32:14 -07:00
Enot (ded) Skelly ddcda4ba5a keep multiline editing 2026-04-17 14:07:00 -07:00
ericz b572314ae9 respect smaz encoding in message byte length calculation. 2026-04-15 09:04:08 -07:00
Enot (ded) Skelly e97fb9bd24 add byte counted text input
adds a new widget that counts bytes during entry

configurable limit and shows user both count and limit

provides color feedback

use new widget in chat and channel text entry
2026-04-15 09:04:08 -07:00
zjs81 a4bbeffddc Merge pull request #386 from zjs81/dev_translations
Dev translations
2026-04-14 21:38:52 -07:00
zjs81 37ec8f2f05 Add localization for chat and repeater features in multiple languages
- Added translations for "Send message", "Guest information", and "Guest tools" in Bulgarian, German, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated the "Clock synchronization after login" feature subtitle in all affected languages.
- Removed untranslated keys from the untranslated.json file as they have now been localized.
2026-04-14 21:38:12 -07:00
zjs81 39cd6d5514 Merge pull request #385 from zjs81/dev
merge dev to main
2026-04-14 21:04:04 -07:00
zjs81 44eb4fad58 Merge pull request #361 from zjs81/unused-plugin
remove unused macos path_provider_foundation
2026-04-14 21:02:30 -07:00
zjs81 1a209cbcfc Merge pull request #372 from zjs81/group-elem
fix: settings dialog lists
2026-04-14 21:02:08 -07:00
zjs81 33a8f34463 Merge pull request #365 from zjs81/rpt-guest
enh: make repeater admin guest aware
2026-04-14 20:44:14 -07:00
zjs81 ce8e8f0d5b Merge pull request #384 from zjs81/clear_toast
clear toast on tap
2026-04-14 20:42:23 -07:00
Enot (ded) Skelly aa2d0f1927 clear toast on tap
this adds a generator showDismissibleSnackBar which by default allows
tapping to clear snack bar toasts. all SnackBar properties are still
available and the

all callers should now use showDismissibleSnackBar() instead of calling
ScaffoldMessenger.of(context).showSnackBar(SnackBar())
2026-04-14 12:01:42 -07:00
Ded 0757c8e53a Merge pull request #369 from just-stuff-tm/auto-time-sync-349
add auto clock synchronization setting after repeater login
2026-04-13 08:20:01 -07:00
Enot (ded) Skelly add4731d05 fix: settings dialog lists
switched to using RadioListTile instead of ListTile to be more accessible
2026-04-10 15:11:44 -07:00
Enot (ded) Skelly 7dc162d968 temp
translations fix
2026-04-10 14:15:14 -07:00
just-stuff-tm 8ba4bbfbc5 add auto clock synchronization setting after repeater login
Introduced a new setting for automatic clock synchronization after a successful repeater login.
Added localization support for the new feature in multiple languages (Bulgarian, German, English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, Chinese).
Implemented storage service methods to manage the new setting.
Updated the repeater settings screen to include a toggle for the new feature.
Enhanced the repeater login dialog to trigger clock synchronization automatically if the setting is enabled.
2026-04-10 14:25:53 -04:00
Ded cac6abfef1 Fix dev
rebase dev over main and resolve merge conflicts
2026-04-09 10:12:47 -07:00
Enot (ded) Skelly 5354acb1d3 clean up after merge conflicts 2026-04-09 09:57:46 -07:00
Enot (ded) Skelly fae416fb34 Merge branch 'dev' of github.com:zjs81/meshcore-open into dev 2026-04-09 09:50:36 -07:00
Enot (ded) Skelly 69433b6d89 small clean up from PR #275
just removes extraneous assignment to _lastNonRepeatSnapshot and moves
the Navigator pop to after all uses of the context in _RadioSettingsDialog
2026-04-09 09:41:02 -07:00
just-stuff-tm ea3b9609fc fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging
- Replace floating-point epsilon frequency comparison with integer Hz
- Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot
  conversion methods on _RadioSettingsSnapshot
- Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions
- Gate _logRadioSettingsState behind kDebugMode
- Use integer Hz in == and hashCode for _RadioSettingsSnapshot

Addresses code review findings on preset/off-grid repeat toggle PR.
2026-04-09 09:41:02 -07:00
just-stuff-tm 20a9939314 fix(settings): scope repeat preset memory to saved state 2026-04-09 09:41:02 -07:00
just-stuff-tm c7b7deb0f6 fix(settings): preserve preset across off-grid repeat 2026-04-09 09:41:02 -07:00
just-stuff-tm 82e04e8090 Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183"
This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527.
2026-04-09 09:41:02 -07:00
Enot (ded) Skelly f299608296 use l10n strings for discovered menu item 2026-04-09 09:41:02 -07:00
ericz 7dcec5b4ee moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. 2026-04-09 09:41:02 -07:00
ericszimmermann e4684b585a codex suggested fix: explicit check if contact location is not null
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-09 09:41:02 -07:00
ericz 8386f262e1 reimplement location aware snr-indikator after alpha7 2026-04-09 09:41:02 -07:00
Enot (ded) Skelly 45cd8a56a3 add jni to generated plugins
linux and windows were missing jni which was being added on fresh builds from dev
2026-04-09 09:41:02 -07:00
Enot (ded) Skelly 754f8a6c62 add fvm directory and rc file to gitignore 2026-04-09 09:41:02 -07:00
Enot (ded) Skelly c4f54efd77 add tooltip to send message buttons 2026-04-09 09:41:02 -07:00
Winston Lowe 637e08d22c Update ML timeout handling and adjust distance threshold for path hops 2026-04-09 09:40:24 -07:00
Winston Lowe 32dc0fca22 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-09 09:40:06 -07:00
n-kam b5aa294fc1 make unread badge max out at 9999+ not 99+ 2026-04-09 09:30:25 -07:00
Winston Lowe 26516baf67 Update ML timeout handling and adjust distance threshold for path hops 2026-04-09 09:30:25 -07:00
Winston Lowe 4879b136f8 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-09 09:30:25 -07:00
Enot (ded) Skelly bdd7fc0cdd remove unused macos path_provider_foundation
added in #299 but appears not needed, flutter removes when building
2026-04-08 14:56:34 -07:00
Ded 5ea044af10 Merge pull request #358 from zjs81/followup-#275
small clean up from PR #275
2026-04-08 10:31:17 -07:00
Enot (ded) Skelly 9d20be1c06 small clean up from PR #275
just removes extraneous assignment to _lastNonRepeatSnapshot and moves
the Navigator pop to after all uses of the context in _RadioSettingsDialog
2026-04-08 10:23:57 -07:00
Ded 9436c2d45a Merge pull request #275 from just-stuff-tm/enhancement/preset-offgrid-repeat-toggle
Enhancement/preset offgrid repeat toggle
2026-04-08 10:10:08 -07:00
Ded 17e55e96bb Merge pull request #357 from zjs81/discovered-text
use l10n strings for discovered menu item
2026-04-08 10:03:10 -07:00
Enot (ded) Skelly e4cfbb57b4 use l10n strings for discovered menu item 2026-04-08 10:01:45 -07:00
Ded d9f9ff58b4 Merge pull request #299 from ericszimmermann/ez_location-snr
reimplement location aware snr-indikator after alpha7
2026-04-08 09:07:02 -07:00
Ded a059f1be45 Merge pull request #356 from zjs81/gen-plug-jni
add jni to generated plugins
2026-04-08 08:38:35 -07:00
Enot (ded) Skelly 9e46f8b44c add jni to generated plugins
linux and windows were missing jni which was being added on fresh builds from dev
2026-04-08 08:37:50 -07:00
Ded a934781009 Merge pull request #355 from zjs81/ignore-fvm
add fvm directory and rc file to gitignore
2026-04-08 08:36:31 -07:00
Enot (ded) Skelly 5fe6738f25 add fvm directory and rc file to gitignore 2026-04-08 08:35:20 -07:00
Ded c1bcf261d7 Merge pull request #353 from zjs81/more-tooltips
add tooltip to send message buttons
2026-04-08 08:32:17 -07:00
Enot (ded) Skelly b570539a2d add tooltip to send message buttons 2026-04-08 08:22:13 -07:00
Ded 89a14c2719 Merge pull request #347 from zjs81/add-contribution
init contributing.md
2026-04-07 14:37:35 -07:00
Enot (ded) Skelly 4ad01ed43c init contributing.md 2026-04-07 13:01:46 -07:00
zjs81 ffaa4033ae Merge pull request #321 from just-stuff-tm/main
Add additional device name prefixes to MeshCoreUuids
2026-04-06 23:04:29 -07:00
zjs81 1a4fd1b477 Merge pull request #339 from ericszimmermann/ez_fix_coordinates
Preserve Coordinates with contact.copyWith() function
2026-04-06 22:58:21 -07:00
zjs81 e1555ce380 Merge pull request #337 from interfect/lowmesh
Add LowMesh prefix and explain how to add more
2026-04-06 22:51:44 -07:00
zjs81 c7933d363b Merge pull request #342 from interfect/graceful-gif-render
Support receiving more formats of GIF message
2026-04-06 14:28:19 -07:00
Zach 08ffb978cf fix: gif trnslat 2026-04-06 14:26:42 -07:00
Adam Novak c5ec60638c Put reaction and GIF helpers in charge of encoding 2026-04-06 02:09:40 -04:00
Adam Novak 75ec3b6116 Centralize GIF parsing in a helper like for reactions 2026-04-06 01:57:51 -04:00
Adam Novak 45c9823c6f Escape forward slashes in regexes 2026-04-05 22:51:48 -04:00
Adam Novak 45658a7612 Understand more kinds of Giphy reference as GIF
This adds Giphy page URLs and `media.giphy.com` URLs (with and without
protocols) as *accepted* encodings for GIF messages, alongside the `g:`
syntax.

When someone posts such a URL by itself as a message, it will be rendered inline just like `g:` messages are now.

This does not change the encoding that GIF messages are *sent* in; that
is still the `g:` syntax.
2026-04-05 22:39:20 -04:00
Winston Lowe a14833494e Merge branch 'dev' of github.com:zjs81/meshcore-open into dev 2026-04-05 12:27:38 -07:00
n-kam 457b44de3a make unread badge max out at 9999+ not 99+ 2026-04-05 12:17:16 -07:00
Winston Lowe 36d4a10396 Update ML timeout handling and adjust distance threshold for path hops 2026-04-05 12:17:15 -07:00
Winston Lowe 77566b0fe1 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-05 12:17:15 -07:00
ericz 7633327f45 Previously, the merge only preserved path override fields and could overwrite existing GPS with null when the incoming frame had 0,0 coordinates.
Now it also preserves prior coordinates when the incoming update omits location.
2026-04-05 14:06:23 +02:00
Adam Novak 6b4b2d7ce6 Add LowMesh prefix and explain how to add more 2026-04-04 19:40:39 -04:00
zjs81 10b63e0df2 Merge pull request #334 from zjs81/Local-LLM-Translator
Local llm translator
2026-04-02 22:57:17 -07:00
zjs81 ba6d751346 #256 finalize translation service 2026-04-02 22:52:52 -07:00
zjs81 96d222a580 fix: update translation model ID retrieval and improve file name extraction in translation service 2026-04-02 22:38:31 -07:00
zjs81 01ad8471cc fix: improve message sending logic and handle range download errors in translation service 2026-04-02 19:52:43 -07:00
zjs81 2b826757cb feat: add translation strings for message translation feature 2026-04-02 19:18:19 -07:00
zjs81 9bf649e2c6 feat: add message translation support
- Introduced translation functionality in chat screen, allowing users to translate messages before sending.
- Added MessageTranslationButton to the input bar for enabling/disabling translation.
- Implemented translation service to handle incoming and outgoing text translations using llama models.
- Enhanced message storage to include original and translated text, language codes, and translation status.
- Created UI components for displaying translated messages and managing translation options.
- Added translation model management, including downloading and storing models locally.
- Updated app settings to manage translation preferences and model selections.
2026-04-02 19:09:17 -07:00
zjs81 c7a2bf9a95 Merge pull request #316 from n-kam/unread-badge-max-value
Make unread badge max out at 9999+ instead of 99+
2026-04-01 22:47:31 -07:00
zjs81 82adbd761b Merge pull request #313 from thesebas/pl-lang
new labels fixed polish translations
2026-04-01 22:46:39 -07:00
zjs81 9a8bdf00dc Merge pull request #326 from spfmoby/better-french-translations
Better french translations
2026-04-01 22:45:12 -07:00
zjs81 8b30342113 Merge pull request #329 from dennis1248/main
Update Dutch translations
2026-04-01 16:51:11 -07:00
Winston Lowe 817c60a155 Update ML timeout handling and adjust distance threshold for path hops 2026-03-31 19:02:29 -07:00
Dennis ten Hoove f08e86cf97 Update Dutch translations 2026-03-31 20:09:26 +02:00
spfmoby a6bb9490a1 Better french translations 2026-03-30 09:17:28 +02:00
just-stuff-tm e4e8bfa4ef Add additional device name prefixes to MeshCoreUuids 2026-03-28 12:20:27 -04:00
ericz d1e45fc2ba moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. 2026-03-28 17:08:59 +01:00
ericz 32fa96431e Merge branch 'main' into ez_location-snr 2026-03-28 17:01:38 +01:00
just-stuff-tm 1e9508d401 fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging
- Replace floating-point epsilon frequency comparison with integer Hz
- Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot
  conversion methods on _RadioSettingsSnapshot
- Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions
- Gate _logRadioSettingsState behind kDebugMode
- Use integer Hz in == and hashCode for _RadioSettingsSnapshot

Addresses code review findings on preset/off-grid repeat toggle PR.
2026-03-27 11:49:59 -04:00
just-stuff-tm 36697c6e61 fix(settings): scope repeat preset memory to saved state 2026-03-27 11:49:59 -04:00
just-stuff-tm c9145c99d3 fix(settings): preserve preset across off-grid repeat 2026-03-27 11:49:59 -04:00
just-stuff-tm 6b6d9caeeb Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183"
This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527.
2026-03-27 11:49:59 -04:00
Winston Lowe d0e3767db6 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-03-26 22:28:01 -07:00
n-kam f9cb0c80a5 make unread badge max out at 9999+ not 99+ 2026-03-27 01:39:52 +03:00
thesebas a26d14bd46 new labels fixed polish translations 2026-03-25 08:36:09 +01:00
zjs81 411cd3f8d2 Merge pull request #270 from just-stuff-tm/fix/linux-ble-pairing-flow
Fix/linux ble pairing flow
2026-03-24 17:48:07 -07:00
just_stuff_tm 38f4de80b6 Refactor Bluetooth pairing localization strings across multiple languages
- Reintroduced Bluetooth pairing PIN title, prompt, show, and hide strings in English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated localization files to ensure consistency and clarity in user prompts related to Bluetooth pairing.
2026-03-24 22:21:23 +00:00
just_stuff_tm 7de07c023f Merge branch 'main' into fix/linux-ble-pairing-flow 2026-03-24 02:24:28 -04:00
zjs81 c272c60f9a Formatted file 2026-03-23 22:37:05 -07:00
zjs81 eca78453d6 Remove debug print statements from MeshCoreConnector, MessageRetryService, and UsbSerialService and fix wrong retry being credited 2026-03-23 22:26:51 -07:00
zjs81 3754cf14ea Bump version to 7.0.0+9 in pubspec.yaml 2026-03-23 19:50:52 -07:00
zjs81 834850fb51 Add companion radio stats, adaptive backoff, path hash width, and UI improvements
- Companion radio stats: poll and display noise floor, RSSI, SNR, airtime
  with dedicated ValueNotifier and ref-counted polling
- Adaptive RF-aware TX backoff based on radio conditions instead of fixed 5s
- Variable-width path hash support (1-3 bytes per hop)
- Air activity dot indicator in app bar with tap to open stats screen
- Jump to oldest unread setting for chat screens
- 1s send cooldown on DM and channel messages
- Link style: theme-aware orange, added EmailLinkifier
- New languages: Hungarian, Japanese, Korean
- Remove dead DeviceScreen and BatteryIndicatorChip
- Remove wakelock_plus dependency
- TX power fields now read as signed int8
2026-03-23 19:26:05 -07:00
zjs81 e7e2bb91b8 Add radio statistics and localization updates
- Implemented radio statistics features in multiple screens including chat, channels, and settings.
- Added localization for new strings in Swedish, Ukrainian, and Chinese.
- Introduced a setting to jump to the oldest unread message in chat and channels.
- Enhanced path management and display for contacts and messages.
- Updated app settings to include new boolean for jumping to the oldest unread message.
- Improved battery indicator and radio stats display in the app bar.
- Removed unused wakelock_plus dependency and updated plugin registrations.
2026-03-23 19:24:27 -07:00
zjs81 4c492f69ef Merge pull request #218 from zjs81/dev-mapOverlap
Show overlaps in public keys of repeaters
2026-03-23 18:51:14 -07:00
zjs81 50f2a8b439 Merge pull request #311 from zjs81/dev
Merge pull request #310 from zjs81/main
2026-03-23 18:50:02 -07:00
zjs81 2c8a15538e Merge branch 'main' into dev-mapOverlap 2026-03-23 18:49:19 -07:00
zjs81 68eeefa04e Merge pull request #307 from ericszimmermann/ez_location_channel_message_path
location aware channel_message_path
2026-03-23 18:47:06 -07:00
zjs81 ebbc367fec Merge pull request #310 from zjs81/main
merge dev
2026-03-23 18:46:40 -07:00
zjs81 2da8995d0b Merge branch 'dev' into main 2026-03-23 18:46:24 -07:00
zjs81 1c376b0056 Merge pull request #309 from zjs81/dev-unifiedData
Unified packet parsing to use BufferReader
2026-03-23 18:41:38 -07:00
zjs81 da70d5fc08 Merge pull request #29 from thesebas/patch-1
Update Polish localization strings for clarity
2026-03-23 18:40:34 -07:00
thesebas f63bc4b787 some minor adjsts 2026-03-23 23:11:51 +01:00
thesebas 9b1f1e1994 make the 'lastSeen' labels shorter to not break the contacts list layout 2026-03-23 23:07:00 +01:00
thesebas 5f475fce4d use correct translation for Advert in another few places 2026-03-23 22:53:09 +01:00
Winston Lowe 0228c38621 fix: Update battery voltage reading and adjust path length handling in ChannelMessage 2026-03-23 11:24:33 -07:00
Winston Lowe fc7283f076 Update lib/l10n/app_bg.arb
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 11:18:37 -07:00
thesebas 7eff1df6e2 use correct word for repeater 2026-03-23 18:47:18 +01:00
Winston Lowe 58252b8a40 fix: Correct return type of _manualAckHash and improve hash computation 2026-03-23 10:14:30 -07:00
Winston Lowe 630606acdc Update byte skipping logic and improve clarity in MeshCoreConnector and ChannelMessage 2026-03-23 08:14:46 -07:00
thesebas bd030153c1 update new labels 2026-03-22 21:58:36 +01:00
thesebas 5140ff383d fix plural form of the label 2026-03-22 21:52:04 +01:00
thesebas dc57f9b9c0 fix missing labels 2026-03-22 21:52:04 +01:00
thesebas 53cd3f4461 Some additional label adjustments 2026-03-22 21:52:04 +01:00
thesebas 35e296f1cd Fix rebase merge error 2026-03-22 21:51:43 +01:00
thesebas 532401cc94 Refactor code structure for improved readability and maintainability 2026-03-22 21:51:19 +01:00
thesebas 5321974cbb Update Polish localization strings for consistency and clarity 2026-03-22 21:51:19 +01:00
thesebas 7c16dde989 Update Polish localization strings 2026-03-22 21:51:19 +01:00
Sebastian Szymbor 9a75c912af Update Polish localization strings for clarity 2026-03-22 21:51:19 +01:00
Winston Lowe 767dc1164e refactor: Replace string reading methods with CString equivalents and improve error handling 2026-03-22 10:50:11 -07:00
just-stuff-tm 14f3429eb5 fix: correct casing of "WisCore-" in deviceNamePrefixes list 2026-03-21 21:07:56 -04:00
just-stuff-tm e49e80d330 style: format deviceNamePrefixes list for better readability 2026-03-21 20:59:54 -04:00
just-stuff-tm d07372c7e0 feat: add MeshCoreUuids class for UUID constants and device name prefixes 2026-03-21 20:59:54 -04:00
just-stuff-tm 990f2bd33d addressed copilot issues still need pr #301 for smoke tests to pass 2026-03-21 20:59:54 -04:00
just-stuff-tm 29660d520e feat: Linux BLE pairing support via bluetoothctl
Add Linux BLE pairing helper that drives bluetoothctl for pair/trust/PIN
entry, with Completer-based flow control, explicit retry loop, and named
timeout constants.

- LinuxBlePairingService: pair-and-trust with up to 2 retries
- LinuxBleErrorClassifier: map bluetoothctl stderr to user-facing errors
- Conditional import stub for web builds (dart.library.io gate)
- Scanner screen: PIN dialog integration for Linux pairing flow
- MeshCoreConnector: Linux pairing/recovery/reconnect wiring
- l10n: 4 new pairing keys across all 14 locales
- 12 unit tests (pairing service + error classifier)
2026-03-21 20:59:53 -04:00
Winston Lowe dbefb0b5f4 feat: Enhance MeshCoreConnector with storage metrics and improve error handling
- Added storageUsedKb and storageTotalKb properties to MeshCoreConnector.
- Updated battery and storage frame parsing with improved error handling.
- Refactored log RX data handling to use BufferReader for better readability and error management.
- Enhanced message parsing in ChannelMessage and Message classes to utilize BufferReader.
- Introduced new text type for signed messages in meshcore_protocol.dart.
- Updated BLE debug log screen to use BufferReader for payload parsing.
- Refactored message retry service to handle ACK hashes as integers instead of Uint8List.
- Improved message storage serialization and deserialization to accommodate new expectedAckHash type.
- Added wasPulled property to Contact model for better state management.
2026-03-21 13:01:02 -07:00
Winston Lowe 4f609f160f feat: Add location validation and improve contact latitude/longitude handling 2026-03-21 09:39:03 -07:00
Winston Lowe e313bea3fc Remove unused _sendAdvert method from SettingsScreen 2026-03-21 09:38:52 -07:00
Winston Lowe 77be2b8e6f Refactor code structure for improved readability and maintainability 2026-03-20 18:58:58 -07:00
Winston Lowe c81c3efe7c Add show overlaps in public keys of repeaters functionality and localization support 2026-03-20 18:57:46 -07:00
Winston Lowe cac0cc15eb feat: Enhance privacy settings and telemetry (#308)
* feat: Enhance privacy settings and telemetry

- Implemented telemetry options for contacts, allowing users to enable or disable telemetry data sharing.
- Introduced a clear chat option in the chat interface for better message management.
- Updated the telemetry screen to handle telemetry data for contacts, including battery level.
- Refactored contact settings to include telemetry options and improved UI for better user experience.

* feat: Refactor repeater resolution logic across multiple screens
2026-03-20 18:54:26 -07:00
Winston Lowe 1392c2d00f feat: Enhance privacy settings and telemetry (#308)
* feat: Enhance privacy settings and telemetry

- Implemented telemetry options for contacts, allowing users to enable or disable telemetry data sharing.
- Introduced a clear chat option in the chat interface for better message management.
- Updated the telemetry screen to handle telemetry data for contacts, including battery level.
- Refactored contact settings to include telemetry options and improved UI for better user experience.

* feat: Refactor repeater resolution logic across multiple screens
2026-03-20 18:34:42 -07:00
zjs81 cb63b48b78 Add comprehensive documentation for various app features
- Introduced "Contacts" documentation detailing the contact management system, types, list, search, and tap actions.
- Added "Map & Location" documentation covering map features, interactions, path tracing, and line-of-sight analysis.
- Created "Navigation" documentation outlining app flow, QuickSwitchBar, and device screen interactions.
- Developed "Notifications" documentation explaining notification types, in-app badges, settings, and rate limiting.
- Established "Repeater Management" documentation for managing repeaters and room servers, including CLI access and telemetry.
- Compiled "Scanner & Connection" documentation detailing BLE, USB, and TCP connection processes.
- Formulated "Settings" documentation outlining access, layout, device info, app settings, node settings, actions, debug options, export features, and about section.
2026-03-20 02:24:02 -07:00
zjs81 4ad4a93a20 formatted code 2026-03-20 01:55:08 -07:00
zjs81 4962a48e64 Msg Retry fixes, channel message fixes. Notification fixes. Make more desktop friendly. Enhance retry algo. Fix predicted location clustering add retries to reactions and fix the reactions in private DMS centralize and cleanup code in var areas 2026-03-20 01:54:31 -07:00
ericszimmermann b88e5e647a Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 08:06:22 +01:00
ericszimmermann 87d11c2e6b Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 07:00:16 +01:00
ericz 7b3c099736 reduce zoomlevel 2026-03-18 06:52:08 +01:00
ericz 11cb14a925 focus on hop if you click on one in the legend. 2026-03-17 23:22:23 +01:00
ericszimmermann d2df2b0bed Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 22:23:23 +01:00
ericz 723bf7293c location aware channel_message_path 2026-03-17 21:56:42 +01:00
zjs81 53caec3e14 Merge pull request #301 from just-stuff-tm/fix/tcp-flow-test-missing-provider
fix: provide AppSettingsService in tcp_flow_test
2026-03-16 16:10:29 -07:00
just_stuff_tm 3c440ca3d4 Merge branch 'zjs81:main' into fix/tcp-flow-test-missing-provider 2026-03-15 21:09:02 -04:00
zjs81 8797d8ffde Merge pull request #302 from stphnrdmr/doc/platform-support
Add more explicit platform support table
2026-03-15 15:21:22 -07:00
Stephan Rodemeier faba120823 Add more explicit platform support table
The platform support was a bit vague, this adds a table to better convey
the differences.
2026-03-15 23:01:38 +01:00
just-stuff-tm be690c8194 fix: provide AppSettingsService in tcp_flow_test
TcpScreen.initState reads AppSettingsService from context
to pre-fill host/port fields, but the test helper only
provided MeshCoreConnector. Switch to MultiProvider so
AppSettingsService is also in the widget tree.
2026-03-15 16:48:40 -04:00
ericszimmermann 0ef2194fb0 codex suggested fix: explicit check if contact location is not null
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-15 12:10:47 +01:00
ericz 3664ae34cd reimplement location aware snr-indikator after alpha7 2026-03-15 11:42:46 +01:00
zjs81 64d75dde45 chore: update version to 7.0.0+8 in pubspec.yaml 2026-03-14 18:46:29 -07:00
zjs81 9199aab7f7 Merge pull request #297 from zjs81/dev-improments
Improvements to path tracing and location handling
2026-03-14 18:42:58 -07:00
zjs81 60e8ee0130 fix: simplify method call for writing data in UsbSerialService 2026-03-14 18:41:57 -07:00
zjs81 6dfb7a4b69 fix: auto-add flag parsing, contact cache restore, and USB reconnect
- Fix operator precedence bug in _handleAutoAddConfig where `flags &
  flag != 0` was parsed as `flags & (flag != 0)`, always checking bit 0
  instead of the correct flag bit
- Populate _contacts from cache in loadContactCache() so contacts
  persist across app restarts
- Toggle DTR low→high on USB connect to force device to see a fresh
  connection
- Add 10ms inter-frame delay for USB sends to prevent missed responses
- Deassert DTR before closing USB port on disconnect/dispose

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:41:21 -07:00
zjs81 28a423e0a8 fix: correct location validation and clean up target contact handling
- Fix asymmetric lat/lon validation in _handleContactAdvert (was checking
  longitude != 0 for latitude; now uses (latitude != 0 || longitude != 0)
  for both)
- Remove duplicate targetGuessed assignment in path_trace_map
- Rename public target field to private _targetContact, use local variable
  to avoid unnecessary null-aware operators

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:14:39 -07:00
Winston Lowe 3593cfa843 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:44 -07:00
Winston Lowe dc85e7a41c Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:17 -07:00
Winston Lowe 9265daaf16 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:09 -07:00
Winston Lowe 4b744184c2 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:09:54 -07:00
zjs81 64698e0be6 Merge pull request #295 from ericszimmermann/ez_group_dropdown3
squashed PR for Dropdown Group Menu
2026-03-14 18:05:22 -07:00
zjs81 3dd9037be3 Merge remote-tracking branch 'origin/main' into ez_group_dropdown3
# Conflicts:
#	lib/main.dart
2026-03-14 18:02:31 -07:00
zjs81 566e3aadf8 fix: migrate filter menus to type-safe generics and harden popup dismissal
- Move ContactSortOption/ContactTypeFilter enums to dedicated
  contact_filter_types.dart (re-exported from contact_search.dart)
- Migrate ContactsFilterMenu and DiscoveryContactsFilterMenu to use
  sealed class action types with SortFilterMenu<T> generics, replacing
  int action constants and switch statements
- Guard _closeDropdownAndRun with ModalRoute.isCurrent check to prevent
  accidental dismissal of parent routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:59:48 -07:00
Winston Lowe 06a906f4f7 Enhance location handling and improve path trace functionality across screens 2026-03-14 17:51:24 -07:00
zjs81 054a84031e Merge pull request #296 from zjs81/feature/ml-timeout-prediction
feat: add ML-based adaptive timeout prediction using LinearRegressor
2026-03-14 17:39:22 -07:00
zjs81 fffcff3b74 fix: cancel persist timer on dispose to prevent post-dispose writes 2026-03-14 17:39:01 -07:00
zjs81 b336aedbc5 fix: address PR #296 code review feedback
- Clamp ML predictions between physics floor (raw airtime) and ceiling
  (worst-case formula) so model can never produce unsafe timeouts
- Replace hourOfDay feature with secondsSinceLastRx for network activity
- Remove unused _ContactStats.stdDev and dead model persistence code
- Debounce observation writes (2s) instead of writing on every delivery
- Skip recording observations when pathLength is null to avoid corrupting
  training data
- Add comment explaining global (not per-contact) RX time tracking
- Remove notifyListeners from retrain to avoid unnecessary widget rebuilds
- Run dart format
2026-03-14 17:32:08 -07:00
zjs81 2ee2358ecc feat: add ML-based adaptive timeout prediction using LinearRegressor
Train a linear regression model on actual message delivery times to
predict tighter timeouts, replacing worst-case physics estimates.
Features: path length, message bytes, seconds since last RX, flood mode.
Global model with per-contact blending after 10+ observations per contact.
Falls back to existing physics formula when model has insufficient data.
2026-03-14 16:56:11 -07:00
ericz 86e9b7fe01 squashed commit of ez_group_dropdown 2026-03-15 00:34:09 +01:00
Winston Lowe 24fa78741b add TCP server address and port settings to AppSettings and update TcpScreen 2026-03-14 11:46:05 -07:00
Winston Lowe 79a45c527b Unify contact retrieval by introducing allContacts getter 2026-03-14 11:45:47 -07:00
zjs81 8b280b37be Merge pull request #293 from zjs81/map-set-location-and-connector-improvements
feat: add set-as-my-location from map long-press, connector and UI
2026-03-14 09:55:02 -07:00
zjs81 fa4da979af feat: enhance location update feedback and improve message retry error handling 2026-03-14 09:54:50 -07:00
zjs81 91608ff09e feat: improve message matching logic and update notification IDs for advertisements 2026-03-14 09:44:37 -07:00
zjs81 71f59d23df feat: add set-as-my-location from map long-press, connector and UI improvements
Add "Set as my location" option to the map long-press bottom sheet,
allowing users to set their device position directly from the map.
Includes connector, chat, contacts, and message retry service improvements.
2026-03-14 09:33:37 -07:00
zjs81 e90742be25 Merge pull request #272 from just-stuff-tm/tcp
feat: Add TCP connection support and UI integration
2026-03-13 11:04:11 -07:00
Zach db935a7454 refactor(tcp): promote MeshCoreTcpConnector, fix translations, harden UI
- Replace thin MeshCoreTcpManager facade with a proper MeshCoreTcpConnector
  that owns TcpTransportService and the frame subscription, mirroring
  MeshCoreUsbManager. The connector no longer holds a raw TcpTransportService
  or a _tcpFrameSubscription field.
- Remove hardcoded default host IP from TcpScreen (keep port 5000 hint).
- Disable connect button during scanning state, not just connecting state.
- Fix tcpPortLabel mistranslated as nautical "port/harbor" in de, it, pt,
  nl, sv, sk, sl, zh; fix corrupted Slovak tcpPortHint ("5 000" → "5000").
- Remove unused tcpStatus_connecting string from all 15 locale arb files
  and all generated app_localizations_*.dart files.
- Add extendedPadding to TCP screen FABs to match USB screen.
- Add Key to connect button; update tests to use byKey and assert
  onPressed == null when button is disabled during scanning.
2026-03-13 10:59:09 -07:00
Winston Lowe 1ad5db27ca Merge branch 'main' into tcp 2026-03-12 23:22:30 -07:00
Winston Lowe 81758adc61 Dev discovery (#291)
* Refactor contact handling: replace DiscoveryContact with Contact, update related methods and settings

* Enhance contact handling: include latitude, longitude, and last modified timestamp in contact updates; refactor path handling to accommodate discovered contacts across multiple screens

* Enhance SNRIndicator: include discovered contacts in name resolution for repeaters

* Refactor path handling: replace addReturnPath with buildPath to improve path construction logic and handle target contact types

* Update lib/screens/map_screen.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add localization for "Show Discovery Contacts" in multiple languages and refactor location plausibility check in map screen

* Enhance contact management: update discovered contacts' active status and improve contact handling with flags and raw packet data

* Refactor ChannelsScreen: pass ChannelMessageStore to buildExpandedContent and ensure messages are cleared after channel creation

* Update MapScreen: adjust label zoom threshold and refactor guessed marker building to include labels

* Refactor ChannelsScreen: change channelMessageStore to a private getter and update its usage in buildExpandedContent calls

* Enhance location plausibility check: add latitude and longitude bounds to ensure valid coordinates

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor MeshCoreConnector and related stores: update discovered contacts handling, migrate legacy keys, and set public key in community store

* Refactor MeshCoreConnector and ChannelsScreen: update discovered contacts handling and set public key in community store; enhance location plausibility check in MapScreen

* Update CMD_ADD_UPDATE_CONTACT frame format to include optional latitude and longitude fields

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 23:08:46 -07:00
Winston Lowe c81791cf1e Migrate legacy storage keys to scoped keys in various store classes (#289) 2026-03-12 08:39:17 -07:00
Winston Lowe 1fba5312a2 Refactor storage classes to include companion's public key (#277)
* Refactor storage classes to include public key handling and improve data loading/saving logic

* Remove redundant publicKeyHex handling from ContactDiscoveryStore and fix key reference in saveContacts method

* Remove unused app_logger import from ContactDiscoveryStore

* Add warning log for empty publicKeyHex in saveChannelMessages method

* Add warning log for empty publicKeyHex in clearMessages method

* Migrate legacy storage keys to scoped keys across multiple stores

* Remove legacy unscoped keys during migration in storage classes

* Update lib/storage/contact_store.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 00:14:48 -07:00
just-stuff-tm 2f770bbd53 fix(tcp): reset state on aborted pre-handshake connect 2026-03-10 21:38:35 -04:00
just-stuff-tm 9db79e9d40 test(tcp): harden cancel-race handling and add coverage
- tighten late TCP connect error suppression to manual-cancel disconnecting/disconnected windows
- keep TCP handshake failures surfaced outside explicit cancel flow
- allow TcpScreen connect action when connector is scanning
- add connector-level tests for late-error suppression classifier
- add TcpScreen test covering connect from scanning state
2026-03-10 20:06:05 -04:00
just-stuff-tm 1913a5aa11 fix(tcp): guard connect cancellation race and align USB screen actions
- add connectTcp cancellation guards after socket connect and connect delay so handshake does not proceed when transport/state changed
- ignore late TCP connect errors after manual cancel or transport switch to avoid spurious second disconnect paths
- keep TCP action hidden only on web and show Bluetooth action on USB screen across platforms for navigation consistency
2026-03-10 19:27:39 -04:00
just-stuff-tm 929c1c3d28 fix(tcp): cancel pending connects on disconnect and propagate remote close 2026-03-09 20:39:17 -04:00
just-stuff-tm 7a2bb20bf7 feat: Add TCP connection support and UI integration
- Implemented TCP transport service for native platforms.
- Added TCP connection screen with input fields for host and port.
- Integrated TCP connection options into the scanner and USB screens.
- Updated localization files for new TCP-related strings.
- Added tests for TCP connection flow and error handling.
- Enhanced USB screen to include TCP connection option.
- Improved layout to ensure no overflow in narrow widths for scanner and USB screens.
2026-03-07 20:07:19 -05:00
zjs81 a1b77bb29b Merge pull request #269 from zjs81/dev-latLonFix
Changed contacts latitude and longitude fields to be null until parsed and set
2026-03-07 13:53:09 -07:00
zjs81 4eecfc92dc Merge pull request #252 from just-stuff-tm/feature/usb
Feature/usb
2026-03-07 13:16:39 -07:00
zjs81 90c8cf5f3e Add TODO to switch flserial to official repo 2026-03-07 13:12:45 -07:00
zjs81 06fa176367 Narrow macOS sandbox entitlement to /dev/cu. and /dev/tty. only
The /dev/ prefix granted read/write to all device nodes. The app only
needs access to serial port devices (/dev/cu.* and /dev/tty.*) for USB
LoRa communication.
2026-03-07 13:10:42 -07:00
zjs81 e4285774a0 Merge branch 'main' into feature/usb 2026-03-07 13:03:15 -07:00
zjs81 b2da695102 Run dart format 2026-03-07 13:01:27 -07:00
zjs81 e1327a93c7 Fix contact sync fallback when channel 0 never arrives
On web BLE, contact sync is deferred until channel 0 arrives via
_handleChannelInfo. If channel 0 times out or channel sync completes
without it, _pendingInitialContactsSync stays true and contacts never
load. Add fallback in _cleanupChannelSync to trigger getContacts() if
the flag is still set when channel sync ends.
2026-03-07 13:00:23 -07:00
zjs81 421bc71bb7 Enhance USB port opening and reading logic with improved error handling and debug logging 2026-03-07 12:55:15 -07:00
zjs81 fef73b7b62 Refactor USB screen, add debug logging, fix UI issues
- Rewrite UsbScreen to mirror ScannerScreen patterns (status bar,
  tap-to-connect port list, bottom FABs, SnackBar errors)
- Extract MeshCoreUsbManager from MeshCoreConnector for cleaner
  USB transport ownership
- Add debug logging throughout USB connection flow (connector,
  manager, web/native services)
- Print debug logs to console in debug mode even when app debug
  log setting is disabled
- Localize remaining hardcoded strings (Web Serial Device fallback
  label, USB status bar keys, companion firmware timeout hint)
- Fix Swedish misspelling in translations (stöderliga → stödda)
- Guard Linux notification init against missing D-Bus session bus
- Fix SNRIndicator hit-test error by adding minimum size constraints
- Update USB flow tests for new UI patterns
2026-03-07 12:38:28 -07:00
Winston Lowe 84ec139ce6 Add latitude and longitude fields to contact handling in MeshCoreConnector 2026-03-07 11:02:47 -08:00
Winston Lowe b748b96237 Enhance contact handling logic in MeshCoreConnector to support conditional addition based on auto-add settings (#268) 2026-03-07 01:45:53 -08:00
Winston Lowe c2671ac2ae Refactor data handling of contacts (#267)
* Refactor data handling in MeshCoreConnector and BufferReader for improved readability and efficiency

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix pointer tracking in BufferReader's readCString method

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-07 01:23:46 -08:00
just-stuff-tm 8238b6197f Regenerated localization files 2026-03-07 01:16:04 -05:00
just_stuff_tm 435ba89982 Merge branch 'zjs81:main' into feature/usb 2026-03-06 20:41:58 -05:00
just-stuff-tm 0565cee461 Enhance message merging logic and improve USB port listing 2026-03-06 20:38:03 -05:00
just-stuff-tm ab2b509d6a Merge branch 'main' into feature/usb 2026-03-06 20:31:05 -05:00
zjs81 eba95af31f Merge pull request #259 from ericszimmermann/ez_shorten_lastSeen
Shorten lastSeen for en,de,es,fr
2026-03-06 18:11:26 -07:00
zjs81 04c016cfe1 Merge pull request #266 from zjs81/zjs81-patch-1
Fix formatting of cryptocurrency addresses in README
2026-03-06 15:41:21 -07:00
zjs81 ea2354712d Fix formatting of cryptocurrency addresses in README 2026-03-06 15:41:02 -07:00
zach 7a0b8aad3d Added more crypto payment options 2026-03-06 15:39:54 -07:00
zjs81 bd34bb5e88 Merge pull request #264 from zjs81/dev-guessed-locations
Dev guessed locations
2026-03-06 15:19:03 -07:00
zach 81548fdc21 ai fixes 2026-03-06 15:18:48 -07:00
zach b2770ef028 fix ai suggestions 2026-03-06 15:11:21 -07:00
zach 7c479f9121 Formatted 2026-03-06 15:03:12 -07:00
zach 1f2dfc555b Add guessed node location map keys and translations
Adds map_showGuessedLocations and map_guessedLocation to app_en.arb and translates them across all 14 supported locales. Regenerates dart localizations.
2026-03-06 15:02:37 -07:00
zjs81 8eb6f32fef Merge pull request #239 from zjs81/dev-notifyListener
Implements a debounced notification listener in MeshCoreConnector
2026-03-06 14:52:27 -07:00
zjs81 d96cd34771 Merge pull request #251 from zjs81/dev-discoverScreen
Contact discovery
2026-03-06 11:59:24 -07:00
just-stuff-tm fb58a3262c addressed codex review cleanup 2026-03-05 02:50:38 -05:00
just-stuff-tm f584c4fba0 added linux notification service 2026-03-05 02:26:37 -05:00
just-stuff-tm b5b930646f Update flserial dependency to a specific commit reference 2026-03-05 02:26:37 -05:00
just-stuff-tm 3452bdae8c Refactor test cases for USB flow and port labels for improved readability 2026-03-05 02:26:37 -05:00
just-stuff-tm 25fc9454a8 Add error handling tests for USB connection and listing ports 2026-03-05 02:26:37 -05:00
just_stuff_tm 524558c511 clean 2026-03-05 02:26:37 -05:00
just_stuff_tm 367e47bb1e Fix USB device name matching and correct localization strings 2026-03-05 02:26:37 -05:00
just_stuff_tm 21ff765e41 Refactor USB permission handling and reset initial channel sync flag 2026-03-05 02:26:37 -05:00
just_stuff_tm 38d40ca0a4 Enhance USB error handling and improve user feedback
- Updated the _friendlyErrorMessage method in UsbScreen to provide more user-friendly error messages based on specific PlatformException codes.
- Added localized error messages for various USB-related errors, improving clarity for users.
- Modified the UsbSerialService to rethrow exceptions instead of throwing StateError, allowing for better error propagation.
- Updated the usb_flow_test to reflect changes in the USB display label behavior, ensuring the test accurately describes the functionality.
2026-03-05 02:26:37 -05:00
just_stuff_tm 5b4535d5dc update flserial dependency reference from main to master 2026-03-05 02:26:37 -05:00
Ben Allfree f9b6299620 gitmodule cleanup 2026-03-05 02:26:37 -05:00
just_stuff_tm 7cb84dbf6f Dart Format 2026-03-05 02:26:37 -05:00
just_stuff_tm 44c0670dae Refine USB transport flow
- replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes
2026-03-05 02:26:37 -05:00
Ben Allfree 74da9e82b5 wip 2026-03-05 02:25:46 -05:00
Ben Allfree 63583dadda wip 2026-03-05 02:25:46 -05:00
Ben Allfree 32632669c3 wip 2026-03-05 02:25:46 -05:00
Ben Allfree 3c0c0d1dea wip 2026-03-05 02:25:46 -05:00
Ben Allfree e6c9a3fea7 wip 2026-03-05 02:25:46 -05:00
just_stuff_tm f5154b0033 Improve sender name resolution for room server messages by handling missing room-contact keys 2026-03-05 02:25:46 -05:00
just_stuff_tm 4c7ee3b3b0 Enhance USB serial services with debug logging and reset functionality
- Introduced debug logging in USB serial services for better traceability.
- Added reset method to UsbSerialFrameDecoder to clear buffered data.
- Updated tests to verify the reset functionality of the decoder.
2026-03-05 02:25:46 -05:00
just_stuff_tm c2f544eeba I restored the Web BLE behavior in [meshcore_connector.dart] to the earlier Windows/Chrome-working state aligned with the logic that was present around commit fcef3de57837983a300634aa3e0a77622e945cc2,
What is back:
- Web BLE resets handshake state before connect
- skips `requestMtu()` on web
- retries `discoverServices()` once on the transient web disconnect case
- uses the non-blocking web `setNotifyValue(true)` workaround again
- skips the immediate `SELF_INFO` wait/refresh stack on web BLE
- defers contact loading on web BLE until after channel `0`
- uses the Web-specific bounded `SELF_INFO` retry timer
- re-enables initial channel-sync gating for web BLE
2026-03-05 02:25:05 -05:00
just_stuff_tm 98cdac4309 Refactor MeshCoreConnector to streamline connection handling and remove web-specific logic for contact synchronization... Back to the way it was before.. For some reason the fix worked on my machine but wwhen i built web from upstream it didnt work 2026-03-05 02:25:05 -05:00
just_stuff_tm d6d11eaad2 Update active USB port key and label on connection, notify listeners 2026-03-05 02:25:05 -05:00
just_stuff_tm 3cef9e81b6 Remove unawaited background service start during USB connection initialization 2026-03-05 02:25:05 -05:00
just_stuff_tm 5216e00807 Refactor USB port handling to introduce display labels and improve state management 2026-03-05 02:25:05 -05:00
just_stuff_tm a0feb129e1 Add post-frame callback to disconnect USB transport on dispose if not navigated to contacts 2026-03-05 02:25:05 -05:00
just_stuff_tm f39a22668e Add initial load scheduling and tests for USB screen and frame codec functionality 2026-03-05 02:25:05 -05:00
just_stuff_tm 781090243c Enhance USB functionality by adding request port label management and platform support checks 2026-03-05 02:25:05 -05:00
just_stuff_tm ca5784f3f8 Add post-frame callback to ensure disconnection on dispose when navigation hasn't changed 2026-03-05 02:25:05 -05:00
just_stuff_tm dcad5c586d Refactor USB connection handling to use scheduled closure and improve error management in USB services 2026-03-05 02:25:05 -05:00
just_stuff_tm 4b24506310 Remove unused import of 'dart:typed_data' in usb_serial_service_web.dart 2026-03-05 02:25:05 -05:00
just_stuff_tm 47c4e0fb82 Fix USB permission receiver registration for compatibility with Android Tiramisu 2026-03-05 02:25:05 -05:00
just_stuff_tm c041e05972 Improve error message for unavailable RX characteristic in USB communication 2026-03-05 02:25:05 -05:00
just_stuff_tm 612612795a Update French localization for connection choice subtitle 2026-03-05 02:25:05 -05:00
just_stuff_tm 3cec3dc233 Improve USB disconnection handling and add payload length validation for USB frames 2026-03-05 02:25:05 -05:00
just_stuff_tm 3542adad1d Update USB communication note for clarity in Swedish localization 2026-03-05 02:25:05 -05:00
just_stuff_tm 115689ad95 Improve USB connection handling by preventing connection attempts when already connected 2026-03-05 02:25:05 -05:00
just_stuff_tm 9a0572e8e4 Add payload length validation in USB frame decoder 2026-03-05 02:25:05 -05:00
just_stuff_tm 2d1160d992 Enhance BLE connection handling and improve USB connection messaging
- Wrapped BLE scan and connection methods in try-catch blocks to handle errors gracefully and provide debug output.
- Added retry logic for service discovery on web platforms after transient disconnections.
- Updated USB connection messages in multiple languages to reflect active support on Android and desktop platforms.
- Improved loading indicators for contacts screen to show a spinner during data loading.
2026-03-05 02:25:05 -05:00
just_stuff_tm ee3af52c0f Add initial contacts sync handling for web Bluetooth transport 2026-03-05 02:25:05 -05:00
just_stuff_tm 98f7c3b088 Refactor USB handling to improve connection management and error cleanup 2026-03-05 02:25:05 -05:00
just_stuff_tm f462815775 Refactor USB connection handling and improve notification setup 2026-03-05 02:25:05 -05:00
just_stuff_tm 5f4333398e Enhance Bluetooth scanning and notification handling for web platform 2026-03-05 02:25:05 -05:00
just_stuff_tm c23a1da430 Add web serial support and USB tests 2026-03-05 02:25:05 -05:00
just_stuff_tm 22a53439b1 Initialize USB Supoport for Andriod and Desktop 2026-03-05 02:25:05 -05:00
Winston Lowe 7d8e049745 Enhance message parsing and error handling in MeshCoreConnector (#260)
* Enhance readString method to include Latin-1 fallback for decoding errors

* Refactor _parseContactMessage to improve error handling and message parsing logic

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 22:56:39 -08:00
ericz 3502559fae minus to tilde 2026-03-04 22:49:20 +01:00
ericz e125318137 Shorten lastSeen for en,de,es,fr 2026-03-04 21:41:51 +01:00
Winston Lowe d53465d13b persist discovered contacts when all are removed 2026-03-03 17:57:56 -08:00
Winston Lowe a0efbbe4bd Persist Discovered Contacts when updated 2026-03-03 17:44:28 -08:00
zjs81 bd5db9a9d5 Merge pull request #253 from ericszimmermann/ez_search-displayed-prefix
Allow search for prefix as Displayed in contact list
2026-03-02 18:44:04 -07:00
zjs81 79b17b53a0 Merge pull request #246 from Specter242/codex/signal-ui-consistency
Unify signal indicator UI across RSSI and SNR
2026-03-02 18:42:21 -07:00
ericz 647fe1523e make it that even combination <0x90 is allowed. 2026-03-02 21:42:44 +01:00
ericz b7d5ee5754 Allow search for prefix as Displayed in contact list. 2026-03-02 21:35:16 +01:00
Winston Lowe 38856c67e5 feat: Add functionality to delete all discovered contacts
- Implemented a new method to remove all discovered contacts from the list.
- Added confirmation dialog for deleting all discovered contacts in the discovery screen.
- Updated localization files to include new strings for deleting all discovered contacts.
- Refactored contact import logic to streamline the process.
- Enhanced the discovery handling to notify users appropriately based on settings.
2026-03-02 10:23:14 -08:00
zjs81 6bd3c17cdf Merge pull request #217 from MeshEnvy/chrome/main
enh: Chrome compatibility
2026-03-01 20:02:29 -07:00
zjs81 6d0712c450 Merge pull request #240 from ericszimmermann/ez_removeDevicenameBrackets
Show name of connected companion
2026-03-01 19:48:54 -07:00
Winston Lowe ddeb1edc2e refactor(discovery): simplify sorting logic for last seen contacts 2026-03-01 14:40:26 -08:00
Winston Lowe 8d73602509 add flags for manual contact addition and telemetry mode handling 2026-03-01 14:36:04 -08:00
Winston Lowe fcab69f9f0 refactor(connector): adjust frame length check and simplify contact handling logic
refactor(settings): extract settings sending logic into a separate method
refactor(ble_debug_log_service): remove unused command case for radio settings
refactor(app_bar): update compact width threshold for app bar display
2026-03-01 13:05:57 -08:00
Winston Lowe d2640e1294 feat(localization): update 'overwrite oldest contact' subtitle for multiple languages 2026-03-01 10:52:19 -08:00
Winston Lowe b02225c02e refactor(connector): remove unused radio settings frame and update command constant 2026-03-01 10:41:31 -08:00
Winston Lowe 128e99e3e7 refactor(settings): remove unused import for adaptive_app_bar_title 2026-03-01 10:35:32 -08:00
Winston Lowe 12bf46bba1 feat(localization): update contact settings translations for multiple languages
- Translated contact settings and related strings in Slovenian, Swedish, Ukrainian, Chinese, Dutch, Polish, Portuguese, Russian, and Slovak.
- Added new strings for discovered contacts actions such as adding, copying, and deleting contacts.
- Enhanced the DiscoveryContact model to include a rawPacket field for better data handling.
- Updated the contacts screen to support new actions in the context menu for discovered contacts.
- Improved the contact discovery store to handle the serialization of the new rawPacket field.
2026-03-01 10:13:17 -08:00
Winston Lowe 92d8e7cd0b Refactor contact search functionality to use DiscoveryContact model and simplify query matching 2026-02-28 19:14:22 -08:00
Winston Lowe 75610695c2 Add contact settings and discovery features
- Implemented contact settings in localization files for Swedish, Ukrainian, and Chinese.
- Added new DiscoveryContact model to handle discovered contacts.
- Created DiscoveryScreen to display discovered contacts with filtering and sorting options.
- Integrated contact discovery storage to persist discovered contacts.
- Updated settings screen to include options for automatic contact addition.
- Enhanced app bar and list filter widgets for better user experience.
- Fixed variable naming inconsistencies in contact model.
2026-02-28 19:11:11 -08:00
Specter242 57ea30cae9 Unify signal indicator UI 2026-02-27 14:30:15 -05:00
Winston Lowe e139383335 Add localized search functionality for contacts (#244)
- Introduced new localization keys for searching contacts, users, favorites, repeaters, and room servers in multiple languages.
- Updated localization files for Italian, Bulgarian, German, English, Spanish, French, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Enhanced the contacts screen to dynamically display search hints based on the selected contact type filter.
- Modified the map screen to utilize the new search functionality for contacts without a number.
2026-02-26 22:53:52 -08:00
ZIER 64428294c9 info • Unnecessary use of string interpolation • lib/widgets/app_bar.dart:43:23 • unnecessary_string_interpolations 2026-02-26 08:59:58 +01:00
ZIER e7a8c36bc4 more aesthetically pleasing display of Companionname 2026-02-26 08:51:57 +01:00
Winston Lowe 2a62390903 Implement debounced notification listener updates in MeshCoreConnector 2026-02-25 21:58:35 -08:00
Ben Allfree 75d25f6312 Merge branch 'main' into chrome/main 2026-02-24 22:51:51 -08:00
Ben Allfree 2a3119544c Merge branch 'main' of github.com:MeshEnvy/meshcore-open 2026-02-24 22:50:20 -08:00
Ben Allfree fb41a5bf10 Merge branch 'zjs81:main' into main 2026-02-24 22:47:48 -08:00
Ben Allfree d88786bb0f ble filtering 2026-02-24 22:41:03 -08:00
Ben Allfree e3148dd449 Merge main into chrome/main 2026-02-24 21:17:33 -08:00
Ben Allfree 96371c03ae pub lock upate 2026-02-24 21:17:24 -08:00
Ben Allfree cac65face6 Merge main into chrome/main 2026-02-24 21:15:49 -08:00
Ben Allfree a777236cd9 Merge branch 'zjs81:main' into main 2026-02-24 13:26:23 -08:00
Ben Allfree 7740698cde Merge branch 'main' into chrome/main 2026-02-23 15:03:20 -08:00
Ben Allfree c8f93f9902 code cleanup 2026-02-23 04:30:13 -08:00
Ben Allfree c34be44950 merge from chat trace 2026-02-23 04:25:04 -08:00
Ben Allfree 549fc62632 chat fixes 2026-02-23 04:09:27 -08:00
Ben Allfree 53d073d8f2 deprecation fix 2026-02-23 03:43:49 -08:00
Ben Allfree 4975b5366e formatting fixes 2026-02-22 11:34:37 -08:00
Ben Allfree c284e571b0 hide message tracing 2026-02-22 11:27:06 -08:00
Ben Allfree a1ee0789a6 deploy on tag only 2026-02-22 11:04:54 -08:00
Ben Allfree 40ac95e8e6 wrangler deploy 2026-02-22 10:48:22 -08:00
Ben Allfree 377f1df445 fix: browser detection 2026-02-22 10:47:51 -08:00
Ben Allfree 9865a03c53 fix: <enter> to send giphy 2026-02-22 09:20:20 -08:00
Ben Allfree a5555bd606 fix: return cursor to message window after send 2026-02-22 09:16:07 -08:00
Ben Allfree 1b4d31a36e gitignore update 2026-02-22 09:11:49 -08:00
Ben Allfree 8e07440114 BLE fix 2026-02-22 08:38:22 -08:00
Ben Allfree 71129bdf4d chrome BLE load fix 2026-02-22 08:37:07 -08:00
Ben Allfree ab05cf8b3e chrome BLE sync 2026-02-22 08:33:45 -08:00
Ben Allfree 452e5337f0 chrome connect 2026-02-22 08:31:29 -08:00
Ben Allfree 6ac987e7cf select BLE device 2026-02-22 08:10:16 -08:00
Ben Allfree 5522f9a236 BLE select cancel 2026-02-22 08:05:19 -08:00
Ben Allfree b4f79c1aae Merge branch 'enh/filter-ble-at-os' into chrome/main 2026-02-22 07:41:36 -08:00
Ben Allfree b08defcff4 Merge branch 'chrome/4-chrome-required-screen' into chrome/main 2026-02-22 07:40:57 -08:00
Ben Allfree 5676cbd84e chrome required screen 2026-02-22 07:40:40 -08:00
Ben Allfree cf8f01128b filter BLE at OS level 2026-02-22 07:15:09 -08:00
Ben Allfree b5e47ce44f filter BLE at OS level 2026-02-22 07:09:35 -08:00
Ben Allfree 7b2f75047c Merge branch 'chrome/1-readme' into chrome/main 2026-02-22 06:59:05 -08:00
Ben Allfree 6d63e49938 add platforminfo helper 2026-02-22 06:54:27 -08:00
209 changed files with 60773 additions and 4884 deletions
+38
View File
@@ -0,0 +1,38 @@
name: Deploy to Cloudflare Workers
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
# Match local development version which provides Dart 3.11.0
flutter-version: '3.41.2'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Get dependencies
run: flutter pub get
- name: Build Web
run: bun run build
- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
+8
View File
@@ -33,6 +33,9 @@ migrate_working_dir/
pubspec.lock
/build/
/coverage/
# fvm project files
.fvm/
.fvmrc
# Symbolication related
app.*.symbols
@@ -58,6 +61,7 @@ secrets.dart
.DS_Store
.AppleDouble
.LSOverride
macos/Flutter/GeneratedPluginRegistrant.swift
# iOS
**/ios/Pods/
@@ -83,3 +87,7 @@ keystore.properties
# IDE
.vscode/launch.json
.vscode/settings.json
.contextstream/
# Cloudflare Wrangler
.wrangler
View File
+1
View File
@@ -0,0 +1 @@
6.2.4
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+71
View File
@@ -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.
+28 -7
View File
@@ -51,7 +51,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Device Management
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
- **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP
- **Device Settings**: Configure radio parameters, power settings, and network options
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
@@ -75,10 +75,16 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Platform Support
-**Android**: Full support (API 21+)
-**iOS**: Full support (iOS 12+)
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
- 🚧 **Web**: Under construction (Chrome)
| Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web |
|--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:|
| BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌<br>(requires websocket bridge) |
| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
### Dependencies
@@ -144,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
@@ -178,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
@@ -189,6 +204,7 @@ Messages are transmitted as binary frames using a custom protocol optimized for
### App Settings
- **Theme**: System default, light, or dark mode
- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian)
- **Notifications**: Configurable for messages, channels, and node advertisements
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
- **Message Retry**: Automatic retry with configurable path clearing
@@ -231,6 +247,11 @@ If you find MeshCore Open useful and would like to support development, you can
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m`
**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5`
Your support helps maintain and improve this open-source project!
## Acknowledgments
+1 -1
View File
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
ndkVersion = "29.0.14206865"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
+1
View File
@@ -19,6 +19,7 @@
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<application
android:label="meshcore_open"
@@ -1,5 +1,18 @@
package com.meshcore.meshcore_open
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
usbFunctions.configureFlutterEngine(flutterEngine)
}
override fun onDestroy() {
usbFunctions.dispose()
super.onDestroy()
}
}
@@ -0,0 +1,582 @@
package com.meshcore.meshcore_open
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MeshcoreUsbFunctions(
private val activity: FlutterActivity,
) {
private companion object {
const val usbRecipientInterface = 0x01
}
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
private val usbManager by lazy {
activity.getSystemService(Context.USB_SERVICE) as UsbManager
}
private val mainHandler = Handler(Looper.getMainLooper())
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
@Volatile private var eventSink: EventChannel.EventSink? = null
@Volatile private var usbConnection: UsbDeviceConnection? = null
@Volatile private var usbInEndpoint: UsbEndpoint? = null
@Volatile private var usbOutEndpoint: UsbEndpoint? = null
@Volatile private var controlInterface: UsbInterface? = null
@Volatile private var dataInterface: UsbInterface? = null
private var readThread: Thread? = null
@Volatile private var isReading = false
@Volatile private var connectedDeviceName: String? = null
private var pendingConnectResult: MethodChannel.Result? = null
private var pendingConnectPortName: String? = null
private var pendingConnectBaudRate: Int = 115200
private data class PortConfig(
val controlInterface: UsbInterface?,
val dataInterface: UsbInterface,
val inEndpoint: UsbEndpoint,
val outEndpoint: UsbEndpoint,
)
private val permissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
handleUsbDetached(intent)
return
}
usbPermissionAction -> Unit
else -> return
}
val result = pendingConnectResult
val portName = pendingConnectPortName
pendingConnectResult = null
pendingConnectPortName = null
if (result == null || portName == null) {
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error(
"usb_device_missing",
null,
null,
)
return
}
val granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (!granted || !usbManager.hasPermission(device)) {
result.error("usb_permission_denied", null, null)
return
}
openUsbDevice(device, pendingConnectBaudRate, result)
}
}
fun configureFlutterEngine(flutterEngine: FlutterEngine) {
registerUsbPermissionReceiver()
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"listPorts" -> result.success(listUsbPorts())
"connect" -> handleUsbConnect(call, result)
"write" -> handleUsbWrite(call, result)
"disconnect" -> {
scheduleCloseUsbConnection {
result.success(null)
}
}
else -> result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
.setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
},
)
}
fun dispose() {
closeUsbConnection()
usbIoExecutor.shutdownNow()
try {
activity.unregisterReceiver(permissionReceiver)
} catch (_: IllegalArgumentException) {
}
}
private fun registerUsbPermissionReceiver() {
val filter =
IntentFilter().apply {
addAction(usbPermissionAction)
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
activity.registerReceiver(permissionReceiver, filter)
}
}
private fun listUsbPorts(): List<String> {
return usbManager.deviceList.values.map { device ->
val productName = device.productName ?: "USB Serial Device"
val vendorProduct =
String.format(
Locale.US,
"VID:%04X PID:%04X",
device.vendorId,
device.productId,
)
"${device.deviceName} - $productName - $vendorProduct"
}
}
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
val portName = call.argument<String>("portName")
val baudRate = call.argument<Int>("baudRate") ?: 115200
if (portName.isNullOrBlank()) {
result.error("usb_invalid_port", null, null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error("usb_device_missing", null, null)
return
}
if (usbManager.hasPermission(device)) {
openUsbDevice(device, baudRate, result)
return
}
if (pendingConnectResult != null) {
result.error("usb_busy", null, null)
return
}
pendingConnectResult = result
pendingConnectPortName = portName
pendingConnectBaudRate = baudRate
val permissionIntent = PendingIntent.getBroadcast(
activity,
0,
Intent(usbPermissionAction).setPackage(activity.packageName),
pendingIntentFlags(),
)
usbManager.requestPermission(device, permissionIntent)
}
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
val data = call.argument<ByteArray>("data")
val connection = usbConnection
val endpoint = usbOutEndpoint
if (data == null) {
result.error("usb_invalid_data", null, null)
return
}
if (connection == null || endpoint == null) {
result.error("usb_not_connected", null, null)
return
}
usbIoExecutor.execute {
try {
writeToDevice(data)
mainHandler.post { result.success(null) }
} catch (error: Exception) {
mainHandler.post {
result.error("usb_write_failed", error.message, null)
}
}
}
}
private fun findUsbDevice(portName: String): UsbDevice? {
val devices = usbManager.deviceList.values
val exactMatch = devices.firstOrNull { it.deviceName == portName }
if (exactMatch != null) {
return exactMatch
}
val normalizedName = portName.substringBefore(" - ").trim()
return devices.firstOrNull { it.deviceName == normalizedName }
}
private fun openUsbDevice(
device: UsbDevice,
baudRate: Int,
result: MethodChannel.Result,
) {
usbIoExecutor.execute {
try {
closeUsbConnection()
val config = resolvePortConfig(device)
if (config == null) {
mainHandler.post {
result.error(
"usb_driver_missing",
null,
null,
)
}
return@execute
}
val connection = usbManager.openDevice(device)
if (connection == null) {
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (!connection.claimInterface(config.dataInterface, true)) {
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (config.controlInterface != null &&
config.controlInterface.id != config.dataInterface.id &&
!connection.claimInterface(config.controlInterface, true)
) {
connection.releaseInterface(config.dataInterface)
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
usbConnection = connection
usbInEndpoint = config.inEndpoint
usbOutEndpoint = config.outEndpoint
controlInterface = config.controlInterface
dataInterface = config.dataInterface
configureDevice(connection, config, baudRate)
connectedDeviceName = device.deviceName
startReadLoop()
mainHandler.post {
result.success(null)
}
} catch (error: Exception) {
closeUsbConnection()
mainHandler.post {
result.error("usb_connect_failed", error.message, null)
}
}
}
}
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
var preferredDataInterface: UsbInterface? = null
var preferredInEndpoint: UsbEndpoint? = null
var preferredOutEndpoint: UsbEndpoint? = null
var fallbackDataInterface: UsbInterface? = null
var fallbackInEndpoint: UsbEndpoint? = null
var fallbackOutEndpoint: UsbEndpoint? = null
var preferredControlInterface: UsbInterface? = null
for (interfaceIndex in 0 until device.interfaceCount) {
val usbInterface = device.getInterface(interfaceIndex)
var inEndpoint: UsbEndpoint? = null
var outEndpoint: UsbEndpoint? = null
for (endpointIndex in 0 until usbInterface.endpointCount) {
val endpoint = usbInterface.getEndpoint(endpointIndex)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
continue
}
when (endpoint.direction) {
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
}
}
val hasDataPair = inEndpoint != null && outEndpoint != null
when {
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
preferredControlInterface == null -> {
preferredControlInterface = usbInterface
}
hasDataPair &&
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
preferredDataInterface = usbInterface
preferredInEndpoint = inEndpoint
preferredOutEndpoint = outEndpoint
}
hasDataPair && fallbackDataInterface == null -> {
fallbackDataInterface = usbInterface
fallbackInEndpoint = inEndpoint
fallbackOutEndpoint = outEndpoint
}
}
}
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
}
private fun configureDevice(
connection: UsbDeviceConnection,
config: PortConfig,
baudRate: Int,
) {
val control = config.controlInterface ?: return
val lineCoding =
byteArrayOf(
(baudRate and 0xFF).toByte(),
((baudRate shr 8) and 0xFF).toByte(),
((baudRate shr 16) and 0xFF).toByte(),
((baudRate shr 24) and 0xFF).toByte(),
0, // stop bits: 1
0, // parity: none
8, // data bits
)
val lineCodingResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x20,
0,
control.id,
lineCoding,
lineCoding.size,
1000,
)
if (lineCodingResult < 0) {
throw IllegalStateException("Failed to configure USB line coding")
}
val controlLineResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x22,
0x0001, // DTR on, RTS off
control.id,
null,
0,
1000,
)
if (controlLineResult < 0) {
throw IllegalStateException("Failed to configure USB control line state")
}
}
private fun startReadLoop() {
val connection = usbConnection ?: return
val endpoint = usbInEndpoint ?: return
isReading = true
readThread =
Thread({
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
val buffer = ByteArray(packetSize * 4)
try {
while (isReading) {
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
if (!isReading) {
break
}
if (bytesRead <= 0) {
continue
}
val packet = buffer.copyOf(bytesRead)
mainHandler.post {
eventSink?.success(packet)
}
}
} catch (error: Exception) {
if (isReading) {
mainHandler.post {
eventSink?.error(
"usb_io_error",
error.message ?: "USB serial I/O error",
null,
)
}
scheduleCloseUsbConnection()
}
}
}, "MeshCoreUsbRead").also { thread ->
thread.isDaemon = true
thread.start()
}
}
private fun writeToDevice(data: ByteArray) {
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
var offset = 0
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
while (offset < data.size) {
val chunkSize = minOf(maxPacketSize, data.size - offset)
val chunk = data.copyOfRange(offset, offset + chunkSize)
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
if (bytesWritten != chunkSize) {
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
}
offset += chunkSize
}
}
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
usbIoExecutor.execute {
closeUsbConnection()
if (onComplete != null) {
mainHandler.post(onComplete)
}
}
}
@Synchronized
private fun closeUsbConnection() {
isReading = false
readThread?.interrupt()
if (readThread != null && readThread !== Thread.currentThread()) {
try {
readThread?.join(300)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
readThread = null
val connection = usbConnection
val claimedControl = controlInterface
val claimedData = dataInterface
usbInEndpoint = null
usbOutEndpoint = null
controlInterface = null
dataInterface = null
usbConnection = null
if (connection != null) {
if (claimedControl != null) {
try {
connection.releaseInterface(claimedControl)
} catch (_: Exception) {
}
}
if (claimedData != null && claimedData.id != claimedControl?.id) {
try {
connection.releaseInterface(claimedData)
} catch (_: Exception) {
}
}
try {
connection.close()
} catch (_: Exception) {
}
}
connectedDeviceName = null
}
private fun handleUsbDetached(intent: Intent) {
val detachedDevice =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
}
val detachedName = detachedDevice?.deviceName ?: return
if (pendingConnectPortName == detachedName) {
pendingConnectResult?.error(
"usb_device_detached",
"USB device was removed before the connection completed",
null,
)
pendingConnectResult = null
pendingConnectPortName = null
}
if (connectedDeviceName == detachedName) {
scheduleCloseUsbConnection {
eventSink?.error(
"usb_device_detached",
"USB device was disconnected",
null,
)
}
}
}
private fun pendingIntentFlags(): Int {
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags = flags or PendingIntent.FLAG_MUTABLE
}
return flags
}
}
+6 -1
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+30
View File
@@ -0,0 +1,30 @@
# MeshCore Open - Feature Documentation
MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does.
## Table of Contents
1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection
2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation
3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing
4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries
5. [Channels](channels.md) - Broadcast channels, communities, and channel chat
6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching
7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
## App Overview
MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can:
- **Chat** with other mesh nodes via encrypted direct messages
- **Broadcast** on shared channels (public, hashtag, private, or community-scoped)
- **View nodes on a map** with GPS locations, predicted positions, and path traces
- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings
- **Share contacts** via `meshcore://` URIs and QR codes
- **Configure radio settings** including frequency, power, bandwidth, and spreading factor
- **Cache offline maps** for use without internet connectivity
- **Analyze line-of-sight** between nodes with terrain elevation profiles
+187
View File
@@ -0,0 +1,187 @@
# Additional Features
## GIF Picker (Giphy Integration)
### How to Access
In any chat screen (direct or channel), tap the GIF button in the message input bar.
### What the User Sees
A bottom sheet with a search field and a grid of GIF thumbnails.
### Key Interactions
- On open, loads trending GIFs (G-rated, 25 results)
- Type to search and press the keyboard submit button (search triggers on submit, not on each keystroke). Clearing the search field reloads trending GIFs
- On network/API errors, a "Retry" button is shown in-place
- Tap a GIF to select it — the chat input shows an inline preview with an X button to dismiss
- Send the message to transmit the GIF reference (`g:<giphy-id>`)
- Recipients see the GIF rendered inline via Giphy CDN
- "Powered by Giphy" attribution is always shown at the bottom of the picker
- The bottom sheet occupies 70% of screen height
---
## Localization / Multi-Language Support
### How to Access
App Settings → Appearance → Language
### Supported Languages (15)
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian
### How It Works
- All UI strings go through Flutter's ARB localization system
- Language can follow the system locale or be explicitly overridden
- Changes take effect immediately
---
## Discovered Contacts Screen
### How to Access
From Contacts screen → overflow menu → "Discovered Contacts"
### What the User Sees
A list of nodes heard passively over the air but not yet added as contacts. Each shows:
- Color-coded avatar (by type)
- Name
- Short public key
- Last-seen time
### Key Interactions
- Search bar with debounced filtering
- Sort by last seen or name; filter by type
- **Tap**: Import the contact (adds to your contact list)
- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete
- Overflow menu → "Delete All" (with confirmation)
- Already-known contacts and your own node are filtered out
---
## SMAZ Compression
### What It Is
An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text).
### How to Enable
- **Per contact**: Chat screen → info button → toggle "SMAZ compression"
- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression"
### How It Works
- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed
- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data
- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text
- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages
- Disabled by default
---
## Community QR Scanner
### How to Access
From Channels screen → "+" FAB → "Scan Community QR"
### What the User Sees
A live QR scanner view with instruction text overlay.
### Key Interactions
- Scan a community QR code shared by another member
- On valid scan: confirmation dialog showing community name and ID
- Option to "Add public channel to device" on join
- If already a member: shows an "Already a member" dialog
- Invalid QR: shows an orange error snackbar
---
## Channel Message Path Viewing
### How to Access
In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop).
### What the User Sees
- Summary card: sender, time, repeat count, path type, observed hops
- "Other Observed Paths" section (if multiple paths detected)
- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates
### Actions
- **Radar icon**: Opens path trace map for live trace
- **Map icon**: Opens a map with hop markers and polyline
- **Path dropdown**: Switch between observed path variants (if multiple)
---
## Debug Logging
### BLE Debug Log
**Access**: Settings → BLE Debug Log
Two views:
- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex.
- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary.
### App Debug Log
**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug)
Structured log entries with level (Info/Warning/Error), tag, message, and timestamp.
Both logs support copy-all and clear operations.
---
## Chrome Required Screen
### When It Appears
Automatically shown on web platforms when a non-Chromium browser is detected.
### What the User Sees
A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational.
---
## Path History Service
### What It Does (Background Service)
Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking:
- Hop count and trip time
- Success/failure counts and route weights
- Flood vs. direct discovery
### Path Scoring
Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path.
Used internally for:
- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths
- **Path selection**: Picks the best-scored path for each retry attempt
- **Flood statistics**: Tracks flood vs. direct discovery ratios
---
## Message Retry Service
### What It Does (Background Service)
Handles reliable delivery of outgoing direct messages:
1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued
2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]`
3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...)
4. Each retry may use a different path (via path history diversity window)
5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path
6. Reports RTT and path data for quality learning
7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs
### Configurable Settings (App Settings → Messaging)
- Max retries (210, default 5)
- Clear path on max retry (on/off)
- Auto route rotation with weight parameters
---
## Timeout Prediction (ML)
### What It Does (Background Service)
An ML-based service that predicts expected delivery timeouts:
- Collects delivery observations (path length, message size, time since last RX, delivery time) in a sliding window of up to 100 observations (oldest evicted first)
- Requires **10 minimum observations** before first training. After that, retrains every 5 new observations
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
- Features with zero variance are automatically excluded from training
- Blends per-contact statistics with ML predictions
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
+254
View File
@@ -0,0 +1,254 @@
# BLE Protocol & Data Layer
This is a technical reference for the communication protocol and data architecture.
## Transport Layer
The app supports three transports, all sharing the same command/response protocol:
| Transport | Method | Implementation |
|---|---|---|
| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` |
| USB Serial | Packet-framed serial | `MeshCoreUsbManager` |
| TCP | Packet-framed socket | `MeshCoreTcpConnector` |
### BLE (Nordic UART Service)
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response".
### USB and TCP Framing
Both use a lightweight packet framing codec:
```
TX (host → device): [0x3C][len_lo][len_hi][payload...]
RX (device → host): [0x3E][len_lo][len_hi][payload...]
```
- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming
- Length: 2-byte little-endian, payload only
- Max payload: 172 bytes
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
- USB: 10ms post-write delay between frames
## Connection State Machine
```
enum MeshCoreConnectionState {
disconnected,
scanning,
connecting,
connected,
disconnecting,
}
```
## BLE Connection Lifecycle
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
5. **Enable TX notifications** (up to 3 attempts on native)
6. **Subscribe** to TX characteristic for incoming frames
7. **Initial sync**: device info query, time sync, channel sync
## Auto-Reconnect (BLE Only)
On unexpected disconnection, auto-reconnect with exponential backoff:
- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s...
- Resets on successful connection
- Disabled for manual disconnects
- Not available for USB or TCP
## Protocol Constants
| Constant | Value | Description |
|---|---|---|
| Max frame size | 172 bytes | BLE/USB/TCP payload limit |
| Public key size | 32 bytes | Ed25519 public key |
| Max path size | 64 bytes | Maximum path data |
| Max name size | 32 bytes | Maximum node name |
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
| App protocol version | 3 | Sent in device query |
| Contact frame size | 148 bytes | Fixed-size contact record |
## Command Codes (App → Device)
| Code | Name | Description |
|------|------|-------------|
| 1 | CMD_APP_START | Announce app connection |
| 2 | CMD_SEND_TXT_MSG | Send direct text message |
| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message |
| 4 | CMD_GET_CONTACTS | Request contact list |
| 5 | CMD_GET_DEVICE_TIME | Query device clock |
| 6 | CMD_SET_DEVICE_TIME | Set device clock |
| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement |
| 8 | CMD_SET_ADVERT_NAME | Set node name |
| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact |
| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message |
| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters |
| 12 | CMD_SET_RADIO_TX_POWER | Set TX power |
| 13 | CMD_RESET_PATH | Reset contact path |
| 14 | CMD_SET_ADVERT_LATLON | Set advertised location |
| 15 | CMD_REMOVE_CONTACT | Remove a contact |
| 16 | CMD_SHARE_CONTACT | Share contact to mesh |
| 17 | CMD_EXPORT_CONTACT | Export contact as bytes |
| 18 | CMD_IMPORT_CONTACT | Import contact from bytes |
| 19 | CMD_REBOOT | Reboot device |
| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage |
| 22 | CMD_DEVICE_QUERY | Query device info |
| 26 | CMD_SEND_LOGIN | Login to repeater/room |
| 27 | CMD_SEND_STATUS_REQ | Request repeater status |
| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key |
| 31 | CMD_GET_CHANNEL | Get channel definition |
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
## Response / Push Codes (Device → App)
| Code | Name | Description |
|------|------|-------------|
| 0 | RESP_CODE_OK | Generic success |
| 1 | RESP_CODE_ERR | Generic error |
| 2 | RESP_CODE_CONTACTS_START | Contact list begins |
| 3 | RESP_CODE_CONTACT | Single contact data |
| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete |
| 5 | RESP_CODE_SELF_INFO | Device self-info response |
| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [25]=ack_hash, [69]=estimated_timeout_ms` |
| 7 | RESP_CODE_CONTACT_MSG_RECV | Incoming direct message (v2) |
| 8 | RESP_CODE_CHANNEL_MSG_RECV | Incoming channel message (v2) |
| 10 | RESP_CODE_NO_MORE_MESSAGES | No more queued messages |
| 11 | RESP_CODE_EXPORT_CONTACT | Exported contact data |
| 9 | RESP_CODE_CURR_TIME | Current device time |
| 12 | RESP_CODE_BATT_AND_STORAGE | Battery mV (uint16 LE) + storage used/total (uint32 LE each) |
| 13 | RESP_CODE_DEVICE_INFO | Firmware info |
| 16 | RESP_CODE_CONTACT_MSG_RECV_V3 | Incoming direct message (v3) |
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) |
| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued |
| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded |
| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed |
| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response |
| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet |
| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result |
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
## Data Models
### Contact
32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout:
```
[0] = resp_code
[132] = public key (32 bytes)
[33] = type (1=chat, 2=repeater, 3=room, 4=sensor)
[34] = flags (bit 0 = favorite)
[35] = path_length
[3699] = path (64 bytes)
[100131] = name (32 bytes, null-padded)
[132135] = timestamp (uint32 LE)
[136139] = latitude (int32 LE, × 1e-6 degrees)
[140143] = longitude (int32 LE, × 1e-6 degrees)
[144147] = last_modified (uint32 LE)
```
### Message (Direct)
Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions.
### Channel Message
Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields.
### Channel
Index (07), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels.
### Community
UUID, name, 32-byte secret, hashtag channel list. Shared via QR code.
## Persistence
All data is stored via `SharedPreferences` (JSON-serialized). No SQLite or other database.
| Data | Storage Key Pattern | Scope |
|---|---|---|
| Contacts | `contacts<pubKey10>` | Per device identity |
| Messages | `messages_<pubKey10><contactKey>` | Per device + contact |
| Channel Messages | `channel_messages_<pubKey10><index>` | Per device + channel |
| Channels | `channels<pubKey10>` | Per device identity |
| Channel Order | `channel_order_<pubKey10>` | Per device identity |
| Contact Groups | `contact_groups<pubKey10>` | Per device identity |
| Communities | `communities_v1<pubKey10>` | Per device identity |
| Unread Counts | `contact_unread_count<pubKey10>` | Per device identity |
| Discovered Contacts | `discovered_contacts` | Global |
| App Settings | `app_settings` | Global |
| Path History | `path_history_<contactKey>` | Per contact |
## Auto-Add Configuration Bitmask
Used by `CMD_SET_AUTO_ADD_CONFIG` (58) and `RESP_CODE_AUTO_ADD_CONFIG` (25):
| Bit | Flag | Description |
|-----|------|-------------|
| 0 | 0x01 | Overwrite oldest contact when list is full |
| 1 | 0x02 | Auto-add chat users |
| 2 | 0x04 | Auto-add repeaters |
| 3 | 0x08 | Auto-add room servers |
| 4 | 0x10 | Auto-add sensors |
## Radio Packet Payload Types
Seen inside `PUSH_CODE_LOG_RX_DATA` raw packets:
| Code | Type |
|------|------|
| 0x00 | REQ (request) |
| 0x01 | RESPONSE |
| 0x02 | TXTMSG (text message) |
| 0x03 | ACK |
| 0x04 | ADVERT |
| 0x05 | GRPTXT (group/channel text) |
| 0x06 | GRPDATA (group data) |
| 0x07 | ANONREQ (anonymous request) |
| 0x08 | PATH |
| 0x09 | TRACE |
| 0x0A | MULTIPART |
| 0x0B | CONTROL |
| 0x0F | RAW_CUSTOM |
## State Management
Uses Flutter `Provider` with `ChangeNotifier`. The central state holder is `MeshCoreConnector`, which owns all in-memory collections and fires debounced (50ms) `notifyListeners()` to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand.
### Data Flow
1. Raw frames arrive over BLE/USB/TCP
2. First byte is parsed as response/push code
3. Appropriate model factory (`fromFrame()`) parses the data
4. In-memory collections are updated
5. Storage stores are persisted (async)
6. `notifyListeners()` triggers UI rebuilds
7. Screens read current state via getters
+164
View File
@@ -0,0 +1,164 @@
# Channels
## Overview
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
Up to 8 channels (indices 07) can be active simultaneously on one device.
## How to Access
QuickSwitchBar tab 1 (middle) from any main screen.
## Channel Types
| Type | Icon | Color | Description |
|---|---|---|---|
| Public | Globe | Green | Fixed well-known PSK; any device can join |
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret |
## Channels List Screen
### What the User Sees
- **Search bar** with live text filtering (300ms debounce)
- **Sort/filter button**
- **Scrollable list of channel cards**, each showing:
- Type icon with color coding (purple badge overlay for community channels)
- Channel name (or "Channel N" if unnamed)
- Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}"
- Unread badge (if messages are unread)
- Drag handle (when manual sort is active)
- **"+" FAB** to add a new channel
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware.
### Sorting Options
- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active)
- **AZ**: Alphabetical
- **Latest messages**: Most recent first
- **Unread**: Most unread first
## Adding a Channel
Tap the "+" FAB to open a dialog with six options:
1. **Create Private Channel** — Enter a name (max 31 characters); a random PSK is generated
2. **Join Private Channel** — Enter a name and a 32-hex PSK (non-hex characters like spaces and dashes are silently stripped, so pasted keys with formatting are accepted)
3. **Join Public Channel** — One tap; uses the well-known public PSK (only shown if no public channel exists)
4. **Join Hashtag Channel** — Enter a hashtag name; PSK is derived from the name. If communities exist, choose between regular hashtag (SHA-256) or community hashtag (HMAC)
5. **Scan Community QR** — Opens QR scanner to join a community
6. **Create Community** — Enter a name; generates a random 32-byte secret; optionally adds a community public channel; shows QR code for sharing
## Channel Actions (Long-Press / Right-Click)
| Action | Description |
|---|---|
| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) |
| Mute / Unmute | Toggle push notification suppression for this channel |
| Delete | Remove the channel from the device (confirmation required) |
## Channel Chat
Tap a channel card to open the channel chat screen.
### App Bar
- Type icon (public/private/hashtag)
- Channel name
- Subtitle: "{type} - {N} unread"
### Message Display
- Reverse-scrolling list (newest at bottom)
- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble
- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle)
- Automatic older-message loading on scroll-to-top
- Jump-to-bottom button when scrolled up
- **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset text size
- **Message tracing mode** (when enabled in App Settings): Each bubble additionally shows path prefix bytes (`via XX,YY,...`), a timestamp, and a repeat count icon
### Message Types in Chat
- **Plain text** with linkified URLs
- **GIFs** (`g:{gifId}`) rendered inline via Giphy CDN
- **Location pins** (`m:{lat},{lon}|{label}|`) shown as tappable location cards
- **Reactions** displayed as emoji pills below target messages
### Replies (Channel Chat Only)
- **Mobile**: Swipe an **incoming** message left to trigger reply (with haptic feedback). You cannot swipe your own outgoing messages. Swipe reply is not available on desktop.
- **All platforms**: Long-press → "Reply"
- Reply banner appears above the input bar with the quoted message (tap X to cancel)
- Sent replies are prefixed `@[{senderName}] {text}`
- Received replies show a bordered quote block inside the bubble; tapping scrolls to the original. Reply previews render GIF thumbnails and location pin icons, not just text.
### Message Path Viewing
- **Mobile**: Tap a message bubble to view its routing path
- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
### Context Actions (Long-Press / Right-Click)
| Action | Availability | Description |
|---|---|---|
| Reply | All messages | Triggers reply mode |
| Path | Desktop only | Opens message path view |
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
| Copy | All messages | Copies text to clipboard |
| Delete | All messages | Removes locally (not from mesh) |
### Message Path Viewing
Tap a message bubble to open the Channel Message Path Screen, which shows:
- Each hop in the path as a visual chain
- Known contacts identified by name at each hop
- Observed vs. declared hop counts
- Alternative path variants (if received via multiple paths)
- Map view buttons for geographic path visualization
## Communities
Communities are a layer above channels that provide a private namespace.
### What is a Community?
A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret:
- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]`
- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]`
Outsiders who don't know the secret cannot discover or join community channels.
### Sharing a Community
Communities are shared via QR codes containing a JSON payload:
```json
{"v": 1, "type": "meshcore_community", "name": "...", "k": "<base64url-secret>"}
```
### Managing Communities
From the channels screen overflow menu → "Manage Communities". Opens a draggable scrollable sheet (resizable 3090% of screen height):
- Each community shows its name and a short community ID (first 8 hex characters)
- **Tap a community** to directly show its QR code for sharing
- **Popup menu** per community:
- **Show QR** — displays the QR code for sharing with new members
- **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
## How Channels Differ from Direct Messages
| Aspect | Channels | Direct Messages |
|---|---|---|
| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact |
| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) |
| Sender identity | Plain text prefix in payload | Verified via public key |
| Replies | Supported (swipe or long-press) | Not supported |
| Retry mechanism | No automatic retry | Exponential backoff with path rotation |
+120
View File
@@ -0,0 +1,120 @@
# Chat & Messaging
## Overview
The app supports two chat modes:
- **Direct messages**: Encrypted point-to-point messages to individual contacts
- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md))
This page covers direct messaging. For channel chat, see the Channels documentation.
## How to Access
From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
## Chat Screen Layout
### App Bar
- **Title**: Contact name
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
- **Action buttons**:
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
- **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
- **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
### Message List
- Scrollable list with newest messages at the bottom
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
- Bubble width capped at 65% of screen width
- Hyperlinks rendered as tappable green underlined text
- **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset
- **Jump to bottom**: Floating button appears when scrolled away from the bottom
- **Lazy loading**: Scrolling to top loads older messages from storage
### Input Bar
- **GIF button** (left): Opens GIF picker bottom sheet
- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time
- **Send button** (right): Submits the message
- On desktop: Enter/Numpad Enter also submits
- When a GIF is selected, the text field shows an inline GIF preview with a dismiss button
## Message Types
| Type | Wire Format | Display |
|---|---|---|
| Plain text | Raw UTF-8 string | Inline text with link detection |
| GIF | `g:<giphy-id>` | Inline GIF image from Giphy CDN |
| Location pin | `m:<lat>,<lon>\|<label>\|...` | Location icon + label; tap to open map |
| Reaction | `r:<hash>:<emoji-index>` | Applied to target message as emoji pill |
## Message Status
Outgoing messages display a status indicator:
| Status | Icon | Meaning |
|---|---|---|
| Pending | Grey double-check | Queued, waiting for device to transmit (visually identical to Sent) |
| Sent | Grey double-check | Device confirmed transmission (visually identical to Pending) |
| Delivered | Green double-check | Remote node acknowledged receipt |
| Failed | Red X | All retries exhausted |
### Message Tracing Mode
When enabled in App Settings, additional metadata appears inside each bubble:
- Timestamp (HH:MM)
- Retry count (e.g., "Retry 2 of 4")
- Status icon
- Round-trip time in seconds (if delivered)
## Message Length Limits
- **Direct messages**: 156 bytes (UTF-8) — enforced in real-time by the input formatter
- **Channel messages**: 160 minus sender name length minus 2 bytes for the `"<name>: "` prefix
- Over-length paste shows a snackbar error
## Send Queue
Only one message per contact can be in-flight at a time (to avoid overflowing the firmware's 8-entry ACK table). If you send multiple messages rapidly, they are queued and sent sequentially — each waits for the previous one to be delivered, fail, or exhaust retries before transmitting.
## Retry Mechanism
When a direct message is sent:
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise `3000 + 3000 × path_length` milliseconds (15000ms for flood)
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
5. **Max retries**: Configurable (default 5, range 210)
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
7. If **Clear Path on Max Retry** is enabled (App Settings), the contact's stored routing path is automatically cleared when max retries are exhausted
8. **Auto route rotation**: When enabled (and no manual path override is set), the retry service uses a diversity window to avoid re-using recently tried paths, cycling through known routes on each attempt
### Manual Retry
Long-press a failed message → "Retry" to re-send using the current routing settings.
## Reactions
Add emoji reactions to incoming messages (not your own):
1. Long-press (or right-click on desktop) a message
2. Select "Add reaction" from the context menu
3. Choose from quick emojis (thumbs up, heart, laugh, party, clap, fire) or browse the full emoji picker
4. Reactions appear as pills below the message bubble with emoji and count
5. Pending reactions show at 50% opacity with a spinner
6. Failed reactions show a red retry icon (tap to retry)
## Context Actions (Long-Press / Right-Click)
| Action | Availability | Description |
|---|---|---|
| Add reaction | Incoming messages only | Opens emoji picker |
| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
| Copy | All messages | Copies text to clipboard |
| Delete | All messages | Removes locally (not from mesh) |
| Retry | Failed outgoing messages | Re-sends the message |
| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
+118
View File
@@ -0,0 +1,118 @@
# Contacts
## Overview
The Contacts screen is the primary hub for managing mesh nodes your radio has a relationship with. A "contact" is any node whose cryptographic advertisement has been received — it can be a chat user, repeater, room server, or sensor.
## How to Access
- Automatically shown after connecting to a device
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
- Back navigation from Chat or Settings screens
## Contact Types
| Type | Avatar Color | Icon | Description |
|---|---|---|---|
| Chat | Blue | Chat bubble | Another user's mesh radio |
| Repeater | Orange | Cell tower | A mesh repeater/relay node |
| Room | Purple | Group | A room server for group chat |
| Sensor | Green | Sensors | A sensor device |
## Contact List
Each contact is displayed as a list tile showing:
- **Avatar**: Color-coded circle with type icon (or first emoji of the contact's name if it starts with one)
- **Name**: Contact name (single line)
- **Path label**: "Direct", "N hops", or "Flood" (with forced variants if a path override is active)
- **Public key**: Shortened hex format `<XXXXXXXX...XXXXXXXX>`
- **Unread badge**: Red pill with count (if unread messages exist)
- **Last seen**: Relative timestamp ("Now", "5 mins ago", "2 hours ago", "3 days ago"). For chat contacts, this shows whichever is more recent: the last advertisement time or the last message time
- **Favorite star**: Amber star icon if favorited
- **Location pin**: Grey pin icon if the contact has GPS coordinates
Pull-to-refresh re-fetches the full contact list from the device.
## Search and Filter
A toolbar at the top provides:
**Search**: Matches contact name (case-insensitive) or public key hex prefix. Debounced at 300ms.
**Sort options**:
- Latest Messages (by most recent message)
- Heard Recently (by last seen / last message)
- AZ (alphabetical)
**Filter options**:
- All, Favorites, Users, Repeaters, Room Servers, Unread Only
## Contact Groups
Groups are a client-side organizational feature for grouping contacts.
- **Create a group**: Tap the group dropdown → "+" icon → enter name → select members → Save
- **Edit a group**: Group dropdown → pencil icon next to the group
- **Delete a group**: Group dropdown → trash icon next to the group
- **Filter by group**: Select a group from the dropdown to show only its members
Groups are stored per radio identity (scoped by public key).
**Validation rules**: Group names cannot be empty, cannot be "all" (reserved, case-insensitive), and must be unique (case-insensitive). The group creation dialog includes a built-in search field to filter contacts when selecting members. Creating a new group automatically selects it as the active filter.
## Tap Actions
| Contact Type | Action on Tap |
|---|---|
| Chat / Sensor | Opens ChatScreen for direct messaging |
| Repeater | Shows password login dialog → opens RepeaterHubScreen |
| Room | Shows password login dialog → opens ChatScreen for room chat |
## Long-Press / Right-Click Menu
| Action | Availability | Description |
|---|---|---|
| Path Trace / Ping | Repeaters, Rooms (always); Chat if `pathLength > 0` | Opens PathTraceMapScreen. Label shows "Ping" when no path bytes are known, "Path Trace" otherwise |
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
| Room Login | Rooms only | Login dialog → ChatScreen |
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
| Open Chat | Chat/Sensor | Same as single tap |
| Add/Remove Favorite | All types | Toggles the favorite flag |
| Share Contact | All types | Copies `meshcore://<hex>` URI to clipboard |
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
## App Bar Menus
The Contacts screen has **two separate popup menus** in the app bar:
**Antenna icon menu** (contact sharing):
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
- Flood Advert — broadcasts across the full mesh network
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
**Three-dot overflow menu**:
- Disconnect — disconnects from the device
- Discovered Contacts — opens the DiscoveryScreen
- Settings — opens the Settings screen
## Adding Contacts
### Automatic (Passive)
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
### Import from Clipboard
Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
### Import from Discovered Contacts
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
## Contact Sharing Format
Contacts are shared using the `meshcore://` URI scheme:
```
meshcore://<hex-encoded-advertisement-packet>
```
This contains the node's public key and metadata. Paste it into another MeshCore app to import.
+186
View File
@@ -0,0 +1,186 @@
# Map & Location
## Overview
The Map feature is a full-featured node-location visualization and radio-planning tool built on OpenStreetMap tiles. It is one of the three primary views accessible from the QuickSwitchBar.
## How to Access
- **QuickSwitchBar tab 2** (rightmost) from Contacts or Channels
- **Deep-link from a chat message**: Tapping a shared location pin in a chat opens the map centered on that pin
- **Settings → Offline Map Cache**: Opens the tile cache management screen
## What the Map Displays
### Self Location (Teal Circle)
Your own node's position, obtained from the device firmware. Displayed as a teal `person_pin_circle` icon. Only appears if the device has GPS data or a manually-set location.
### Contact / Node Markers (Color-Coded)
All contacts with known GPS coordinates are plotted:
| Type | Color | Icon |
|---|---|---|
| Chat user | Blue | Person |
| Repeater | Green | Router |
| Room | Purple | Meeting room |
| Sensor | Orange | Sensors |
Node name labels appear automatically at zoom level 12 and above.
### Shared Map Pins (Flag Icons)
Location pins shared in chat messages are displayed as flags:
- **Blue flag**: From a direct message
- **Purple flag**: From a private channel
- **Orange flag**: From a public channel
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
### Predicted / Guessed Locations (Semi-Transparent)
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
#### Why guessed locations exist
In a mesh network, every message hops through one or more repeaters on its way to the destination. Each repeater in the path is identified by the first byte of its public key. If any of those repeaters have a known GPS location (because they advertise it), then a contact that routes through those repeaters must be somewhere within radio range of them. By combining the positions of multiple repeaters a contact is known to use, the app can triangulate a rough area where the contact is likely located.
#### How the algorithm works
1. **Build a repeater index**: The app collects all known contacts of type Repeater that have a valid GPS position and indexes them by the first byte of their public key.
2. **Collect anchor points**: For each contact that lacks GPS, the app looks at the **last-hop byte** of the contact's current path and also searches the `PathHistoryService` for recent paths. Each last-hop byte that matches a located repeater becomes an "anchor point" — a GPS coordinate the contact is likely near.
3. **Resolve ambiguity**: If multiple repeaters share the same first public-key byte (a hash collision), that byte is discarded as ambiguous. Only unambiguous one-to-one matches are kept.
4. **Filter geometric inconsistencies**: Two anchor points separated by more than `2 × maxRangeKm` (the estimated LoRa radio range, computed from the current frequency, bandwidth, spreading factor, and TX power using a free-space path loss model) cannot both be in range of the same node. Outlier anchors are removed to keep only a geometrically consistent set.
5. **Compute the estimated position**:
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
- **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80120m) applied for visual separation.
6. **Assign confidence level**:
- **High confidence** (2+ anchors): Displayed at 55% opacity.
- **Low confidence** (1 anchor): Displayed at 30% opacity.
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
#### How to read guessed locations on the map
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
- **More transparent** (30%): Lower confidence — based on a single repeater anchor only.
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
## Map Interactions
### Zoom and Pan
Standard pinch-to-zoom (range 218). Initial camera position is calculated from the statistical spread of all plotted points.
### Tap on a Node Marker
Opens a dialog showing: type, path (hop chain), coordinates, last-seen time, and public key. Action buttons vary by type:
- **Chat nodes**: "Open Chat"
- **Repeaters**: "Manage Repeater"
- **Rooms**: "Join Room"
### Long-Press on Empty Map Area
Shows a bottom sheet with:
- **Share marker here**: Prompts for a label, then pick a DM contact or channel to send the location to. Wire format: `m:<lat>,<lon>|<label>|poi`
- **Set as my location**: Updates your device's advertised location
### Filter Dialog (FAB)
Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts.
Additional filters:
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
- **Last-seen time slider**: From 1 hour to "all time"
### Legend Card (Top-Right)
Shows node count and pin count. Tappable to expand a legend of all marker types.
---
## Path Trace Map
### How to Access
- From the main map's radar icon
- From a contact's long-press menu → "Path Trace / Ping"
- From a message's path view → radar icon
### What the User Sees
A map with a polyline showing the route from your node through repeater hops to the target:
- **Green circles**: Hops with known GPS coordinates
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
- **Red endpoint**: Target contact with known GPS
- **Purple semi-transparent endpoint**: Target with guessed position
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
### How It Works
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
---
## Line-of-Sight (LOS) Analysis
### How to Access
From the main map, tap the terrain/antenna icon.
### What the User Sees
A full-screen map with a collapsible control panel containing:
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0400 ft), Run LOS button
### Key Interactions
- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable)
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
- **Antenna heights** are adjustable for both endpoints
- **Map line** between endpoints is colored green (clear) or red (blocked)
- Terrain elevation is fetched from the Open-Meteo API (2181 sample points, cached 24 hours)
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
---
## Offline Map Cache
### How to Access
Settings → App Settings → Map Display → Offline Map Cache
### What the User Sees
- Map with a blue polygon overlay showing previously selected cache bounds
- Bounding box coordinates card
- **Cache Area** controls: "Use Current View" and Clear buttons
- **Zoom Range** slider (318) with estimated tile count
- **Download progress** bar (when downloading)
- **Download Tiles** and **Clear Cache** buttons
### Key Interactions
1. Pan/zoom the map to the desired area
2. Tap "Use Current View" to capture the viewport as cache bounds
3. Adjust the zoom range slider
4. Tap "Download Tiles" (confirmation dialog shows estimated count)
5. Tiles are downloaded with up to 8 concurrent connections
6. Once cached, tiles are served from disk without internet (365-day stale period)
---
## GPX Export
### How to Access
Settings → Export section
### What It Does
Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Three export options:
- **Export Repeaters**: Repeater and Room contacts with locations
- **Export Contacts**: Chat contacts with locations
- **Export All**: All contacts with locations
Each waypoint includes: name, lat/lon, type label, and public key hex.
---
## Location Data Sources
The phone's own GPS is **never used**. All location data comes from the mesh:
1. **Device self-location**: Read from firmware device-info response. Set manually in Settings → Location, or updated automatically if the device has a GPS module.
2. **Remote node locations**: Extracted from advertisement packets received over the mesh. Encoded as integer lat/lon × 1,000,000.
+87
View File
@@ -0,0 +1,87 @@
# Navigation
## App Flow
The app follows this general flow:
```
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen
```
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
## Quick Switch Bar
The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual treatment (blur backdrop, transparent theme, rounded corners). It appears at the bottom of all three main screens.
| Index | Icon | Label | Screen |
|---|---|---|---|
| 0 | People | Contacts | ContactsScreen |
| 1 | Tag | Channels | ChannelsScreen |
| 2 | Map | Map | MapScreen |
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
## Device Screen
The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
### What the User Sees
**App Bar**:
- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
- Right: Disconnect button (`bluetooth_disabled` crossed-out icon) and Settings button (tune icon)
**Body**:
- **Connection Card**: Device avatar, device name, device ID, "Connected" chip, and battery chip
- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
### Disconnection
- The disconnect button shows a confirmation dialog before disconnecting
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
## Theme and Locale
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
- **Language** can be overridden to one of 15 supported languages, or follow the system locale
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
## Full Navigation Graph
```
ScannerScreen (root, always on stack)
├─ [BLE connect] → push → ContactsScreen
├─ [TCP FAB] → push → TcpScreen
│ └─ [TCP connected] → pushReplacement → ContactsScreen
└─ [USB FAB] → push → UsbScreen
└─ [USB connected] → pushReplacement → ContactsScreen
ContactsScreen (selected=0)
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
├─ [quick-switch 2] → pushReplacement → MapScreen
├─ [tap contact] → push → ChatScreen
├─ [overflow > Settings] → push → SettingsScreen
└─ [overflow > Discovered] → push → DiscoveryScreen
ChannelsScreen (selected=1)
├─ [quick-switch 0] → pushReplacement → ContactsScreen
├─ [quick-switch 2] → pushReplacement → MapScreen
├─ [tap channel] → push → ChannelChatScreen
└─ [overflow > Settings] → push → SettingsScreen
MapScreen (selected=2)
├─ [quick-switch 0] → pushReplacement → ContactsScreen
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
├─ [radar button] → push → PathTraceMapScreen
├─ [terrain button] → push → LineOfSightMapScreen
└─ [long-press] → share marker / set location
Settings (push from any main screen)
└─ [App Settings] → push → AppSettingsScreen
└─ [Offline Map Cache] → push → MapCacheScreen
```
Any disconnection from any screen triggers `popUntil(route.isFirst)`, returning to the Scanner.
+92
View File
@@ -0,0 +1,92 @@
# Notifications
## Overview
MeshCore Open provides both **system notifications** (push-style OS alerts) and **in-app unread badges** to inform users of new activity.
## Notification Types
### 1. Direct Message Notifications
- **Triggered when**: A new incoming message arrives from a Chat or Room contact
- **Title**: Contact's name
- **Body**: Message text (reactions show "Reacted [emoji]", GIFs show "Sent a GIF")
- **Priority**: High
- **Android channel**: `messages`
### 2. Channel Message Notifications
- **Triggered when**: A new message arrives on a non-muted channel
- **Title**: Channel name (or "Channel N" if unnamed)
- **Body**: `"<senderName>: <message text>"`
- **Priority**: High
- **Android channel**: `channel_messages`
### 3. Advertisement Notifications
- **Triggered when**: A new node is discovered on the mesh for the first time
- **Title**: "New [type] discovered" (e.g., "New chat node discovered")
- **Body**: Contact's name
- **Priority**: Default
- **Android channel**: `adverts`
### 4. Background Service Notification (Android Only)
- A persistent low-priority notification: "MeshCore running — Keeping BLE connected"
- Required by Android for foreground services to keep BLE alive in the background
- Tap to re-launch the app
- **Does not auto-start on reboot** — the user must re-open the app manually after a phone restart
### Notification Tap Behavior
Tapping a notification currently re-launches the app at the root route. It does **not** navigate directly to the relevant chat or channel.
## In-App Unread Badges
Red numeric badges appear throughout the UI:
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
- **Channels list**: Each channel row shows an unread badge
- **Chat screen subtitle**: Shows unread count inline
- Badges cap at "99+" for display
### How Unread Counts Work
- Stored per contact (by public key) and per channel, **scoped to the connected device's identity** (first 10 hex characters of its public key). Switching between different radios gives each its own independent unread state
- **Suppressed when viewing**: Opening a chat resets the count to 0 and cancels the OS notification
- **Ignored for**: Outgoing messages, CLI messages, and repeater contacts
- Debounced writes (500ms) to avoid excessive storage I/O during message bursts
## Notification Settings
Access via **App Settings → Notifications**:
| Setting | Default | Description |
|---|---|---|
| Enable Notifications | On | Master toggle; requests OS permission when turned on |
| Message Notifications | On | DM alerts (greyed out if master is off) |
| Channel Message Notifications | On | Channel alerts (greyed out if master is off) |
| Advertisement Notifications | On | New node alerts (greyed out if master is off) |
### Per-Channel Muting
Long-press a channel in the channels list → "Mute channel" / "Unmute channel". Muted channels do not generate OS notifications.
There is no per-contact muting.
## Rate Limiting
The notification system prevents notification storms:
- **Minimum interval**: 3 seconds between individual notifications
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
## Notification Clearing
- **Opening a contact chat**: Cancels the OS notification and resets unread count
- **Opening a channel**: Cancels the channel notification and resets unread count
- **Opening Contacts screen**: Cancels all advertisement notifications
## Platform Support
| Platform | Message Notifs | Badge | Background Service |
|---|---|---|---|
| Android | Yes | Via notification number | Yes (foreground service) |
| iOS | Yes | Yes (app badge) | No |
| macOS | Yes | Yes | No |
| Windows | Yes | No | No |
| Linux | Yes (if D-Bus available) | No | No |
+186
View File
@@ -0,0 +1,186 @@
# Repeater Management
## Overview
Repeater Management provides tools for administering MeshCore repeater and room server nodes. It includes device status monitoring, CLI access, telemetry reading, neighbor discovery, and remote configuration.
## How to Access
From the Contacts screen:
1. Long-press a **Repeater** or **Room** contact
2. Select "Manage Repeater" or "Room Management"
3. Enter the admin password in the login dialog
4. Navigate to the Repeater Hub Screen
### Login Dialog
- Password field with show/hide toggle
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password stops immediately after the first attempt — only timeouts trigger retries
- After 5 failed attempts, further login attempts are blocked
---
## Repeater Hub Screen
The central management screen showing:
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
- **Battery chemistry selector**: NMC / LiFePO4 / LiPo (saved per repeater)
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" based on contact type:
| Card | Destination |
|---|---|
| Status | Repeater Status Screen |
| Telemetry | Telemetry Screen |
| CLI | Repeater CLI Screen |
| Neighbors | Neighbors Screen |
| Settings | Repeater Settings Screen |
---
## Repeater Status
### What the User Sees
Three information cards:
**System Information**:
- Battery percentage
- Uptime
- Queue length
- Error flags
- Clock at login time
**Radio Statistics**:
- Last RSSI and SNR
- Noise floor
- TX and RX airtime
**Packet Statistics**:
- Packets sent, received, and duplicates
- Broken down by flood vs. direct
### Key Interactions
- Auto-queries the repeater on open; shows a loading spinner until data arrives
- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
- Pull-to-refresh or refresh button to re-query
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
---
## Repeater CLI
A terminal-style interface for sending commands directly to the repeater.
### What the User Sees
- **Quick-command bar** (horizontal scroll): Shortcut buttons for common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
- **Command history list**: Sent commands in primary color, responses in secondary color
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
### Key Interactions
- Type a command and press send (or Enter on desktop)
- Up/down arrows navigate through command history
- Quick-command buttons populate and send common commands
- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
- Clear icon: Wipes the command/response history
- Failed/timed-out commands are automatically retried once
### Available CLI Commands
**General**: `advert`, `reboot`, `clock`, `password`, `ver`, `clear stats`
**Settings**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
**Bridge**: `get bridge.type`
**Logging**: `log start`, `log stop`, `log erase`
**Neighbors**: `neighbors`, `neighbor.remove`
**Region Management**: `region`, `region load/get/put/remove/allowf/denyf/home/save`
**GPS**: `gps`, `gps on/off/sync/setloc/advert`
---
## Telemetry
### What the User Sees
A list of Cayenne LPP sensor channel cards:
- **Channel 1** (special): Battery voltage (shown as percentage or raw mV) and MCU temperature
- **Other channels**: Raw sensor values with appropriate labels
Shows "No data" until a response arrives from the repeater.
### Key Interactions
- Auto-queries on open
- Pull-to-refresh
- Temperature respects metric/imperial setting
- Battery readings are stored for the repeater's battery snapshot
---
## Neighbors
### What the User Sees
A card titled "Repeater's Neighbors - N" listing each neighbor as:
- Repeater name (or hex key prefix if unknown)
- Time since last heard
- SNR quality icon with color coding and label
### Key Interactions
- Auto-queries up to 15 neighbors on open
- Matches public key prefixes against known contacts to show names
- Pull-to-refresh
---
## Repeater Settings
### What the User Sees
Five configuration cards:
**1. Basic Settings**
- Name field
- Admin password field
- Guest password field
**2. Radio Settings**
- Frequency (MHz)
- TX Power (dBm)
- Bandwidth dropdown (kHz)
- Spreading Factor (SF5SF12)
- Coding Rate (4/54/8)
**3. Location Settings**
- Latitude and longitude fields
**4. Features**
- Packet forwarding toggle
- Guest access toggle
**5. Advertisement Settings**
- Local advert interval slider (60240 minutes) with enable/disable toggle
- Flood advert interval slider (3168 hours) with enable/disable toggle
**6. Danger Zone** (red-styled card)
- Reboot repeater
- Erase filesystem (serial-only warning)
### Key Interactions
- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
- Save button appears when changes are detected
- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
- Validation prevents invalid values (e.g., frequency range, LoRa parameter compatibility)
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly
+124
View File
@@ -0,0 +1,124 @@
# Scanner & Connection
## BLE Scanner (Home Screen)
The BLE Scanner is the app's home screen, displayed immediately on launch.
### How to Access
- Opens automatically when the app starts
- Returns here when disconnecting from any device
- Accessible by navigating back from a connected session
### What the User Sees
**App Bar**: Centered title "Scanner".
**Bluetooth-Off Warning Banner** (conditional): Appears when the Bluetooth adapter is off, showing a `bluetooth_disabled` icon, a warning message, and on Android, an "Enable Bluetooth" button.
**Status Bar**: A full-width colored strip reflecting the current connection state:
| State | Text | Color |
|---|---|---|
| Disconnected | "Not connected" | Grey |
| Scanning | "Scanning..." | Blue |
| Connecting | "Connecting..." | Orange |
| Connected | "Connected to \<device name\>" | Green |
| Disconnecting | "Disconnecting..." | Orange |
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
**Bottom FAB Row**: Up to three floating action buttons:
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
### Device Tile
Each discovered device is displayed as a list tile showing:
- **Signal strength icon** (color-coded by RSSI):
- Green: >= -60 dBm (excellent)
- Light green: -60 to -70 dBm (good)
- Amber: -70 to -80 dBm (fair)
- Orange: -80 to -90 dBm (weak)
- Red: < -90 dBm (poor)
- **RSSI value** in dBm (e.g., "-72 dBm")
- **Device name** (falls back to "Unknown Device")
- **Device ID** (BLE MAC address on Android; a system-assigned UUID on iOS/macOS)
- **Connect button** (the entire tile row is also tappable — both trigger connection)
Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon shape and are only differentiated by color (orange vs. red).
### How Scanning Works
- Filters for devices with names starting with `MeshCore-` or `Whisper-`
- Uses low-latency scan mode on Android
- Scans for 10 seconds then auto-stops
- On iOS/macOS, waits for BLE adapter initialization before starting
- If Bluetooth is turned off during a scan, scanning stops immediately
### Connecting to a Device
Tap a device tile or its Connect button:
1. The connector stops scanning and transitions to "connecting"
2. Connects to the device with a 15-second timeout
3. Requests MTU 185 bytes for optimal throughput
4. Discovers BLE services and locates the Nordic UART Service
5. Subscribes to TX notifications for receiving data
6. On success, automatically navigates to the Contacts screen
7. On failure, shows a red error snackbar
---
## USB Connection
### How to Access
From the Scanner screen, tap the **USB** FAB button.
### What the User Sees
- A colored status bar at the top (same color scheme as BLE scanner)
- A list of detected USB serial ports, each showing:
- Friendly display name
- Raw port name (subtitle, only shown when it differs from the display name)
- "Connect" button
- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
### Key Interactions
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
- On mobile: tap the "Scan" FAB to manually refresh
- Tap a port or its Connect button to connect
- On successful connection, navigates to Contacts screen
- On connection failure, the port list automatically refreshes
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
---
## TCP Connection
### How to Access
From the Scanner screen, tap the **TCP/IP** FAB button.
### What the User Sees
- A colored status bar at the top
- **Host address** text field
- **Port number** text field
- **Connect** button
- FABs at the bottom to switch to USB or BLE
### Key Interactions
- Last-used host and port are pre-populated from saved settings
- Tap Connect to validate inputs and connect
- Host must not be empty
- Port must be a number between 1 and 65535
- Validation errors are shown as red snackbars
- The Connect button shows a spinner and "Connecting..." label while in progress
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
- On success, navigates to Contacts screen and saves the host/port to settings
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
- Error messages for timeout, unsupported platform, and connection failures
+169
View File
@@ -0,0 +1,169 @@
# Settings
## How to Access
- From the Device Screen: tap the tune/sliders icon in the app bar
- From Contacts or Channels: overflow menu (three-dot) → Settings
Settings are only accessible while a device is connected.
## Settings Screen Layout
The settings screen is a scrollable list of cards:
1. [Device Info](#device-info)
2. [App Settings](#app-settings) (link to sub-screen)
3. [Node Settings](#node-settings)
4. [Actions](#actions)
5. [Debug](#debug)
6. [Export](#export)
7. [About](#about)
---
## Device Info
A collapsible card showing read-only device information. **Collapsed by default** — tap the header to expand with an animated chevron indicator:
| Field | Description |
|---|---|
| Name | Connected device's display name |
| ID | Device identifier |
| Status | Connected / Disconnected |
| Battery | Percentage or voltage (tap to toggle) |
| Node Name | The node's mesh identity name |
| Public Key | First 16 hex characters + "..." |
| Contacts Count | Number of known contacts |
| Channel Count | Number of configured channels |
Battery shows an alert icon and orange text when at 15% or below. The toggle only works when millivolt data is available from the firmware.
---
## App Settings
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
### Appearance
- **Theme**: System / Light / Dark
- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
### Notifications
- **Master enable/disable**: Requests OS permission when enabling
- **Message notifications**: New direct message alerts
- **Channel message notifications**: New channel message alerts
- **Advertisement notifications**: New node discovery alerts
### Messaging
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
- Max Route Weight (110, default 5, integer steps)
- Initial Route Weight (0.55.0, default 3.0)
- Success Increment (0.12.0, default 0.5, 0.1 steps)
- Failure Decrement (0.12.0, default 0.2, 0.1 steps)
- Max Message Retries (210, default 5)
### Battery
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
### Map Display
- **Show Repeaters**: Toggle repeater markers on map
- **Show Chat Nodes**: Toggle chat node markers
- **Show Other Nodes**: Toggle room/sensor markers
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
- **Units**: Metric / Imperial
- **Offline Map Cache**: Navigate to tile download screen
### Debug
- **App Debug Logging**: Enable the in-app debug log
---
## Node Settings
These settings are sent directly to the connected device firmware.
### Node Name
- Opens a dialog with a text field (max 31 characters)
- Sends the new name to the device
- Confirmed via snackbar
### Radio Settings
Opens a dialog pre-populated with the device's current radio settings. Contains:
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
- **Frequency** (MHz): Free text, validated 3002500 MHz
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
- **Spreading Factor**: SF5SF12
- **Coding Rate**: 4/5, 4/6, 4/7, 4/8
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
### Location
Opens a dialog pre-populated with the device's current coordinates (if known):
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
- If GPS-capable hardware (detected via `gps` custom variable):
- GPS Update Interval (seconds, 6086399, default 900 = 15 minutes). Validated and sent separately before lat/lon
- Enable GPS toggle (takes effect immediately, not deferred to Save)
- Validation: lat ±90, lon ±180
### Contact Settings
Five toggles controlling which node types are auto-added when heard:
- Auto-add Chat Users
- Auto-add Repeaters
- Auto-add Room Servers
- Auto-add Sensors
- Overwrite Oldest (when contact list is full)
### Privacy Mode
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
---
## Actions
One-tap device operations:
| Action | Description |
|---|---|
| Send Advertisement | Floods the mesh with your node's advertisement |
| Sync Time | Sends current Unix timestamp to the device |
| Refresh Contacts | Re-requests the full contact list |
| Reboot Device | Confirmation dialog → reboots the device (shown in orange) |
---
## Debug
Two log viewers accessible via list tiles:
### BLE Debug Log
Two views (togglable via segmented button):
- **Frames view**: Direction icon, description, hex preview, timestamp per frame. Long-press to copy hex.
- **Raw Log RX view**: Decoded LoRa packets with route type, payload type, path, and summary.
- Copy-all and Clear buttons in the app bar.
### App Debug Log
Structured log entries (Info / Warning / Error), with tag, message, and timestamp.
- Must be enabled first in App Settings → Debug
- Copy-all and Clear buttons
---
## Export
Three GPX export options (not available on web):
| Option | Exports |
|---|---|
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
| Export Contacts | Chat contacts with GPS coordinates |
| Export All | All contacts with GPS coordinates |
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
---
## About
Shows the standard Flutter about dialog with app name, version, and legal notice.
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
import 'dart:async';
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/tcp_transport_service.dart';
/// Manages TCP transport for MeshCore devices.
///
/// Owns the [TcpTransportService] and TCP-specific connection state.
/// The main [MeshCoreConnector] delegates all TCP operations here.
class MeshCoreTcpConnector {
final TcpTransportService _service = TcpTransportService();
AppDebugLogService? _debugLog;
StreamSubscription<Uint8List>? _frameSubscription;
// --- Getters ---
String? get activeEndpoint => _service.activeEndpoint;
bool get isConnected => _service.isConnected;
// --- Configuration ---
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({required String host, required int port}) async {
_debugLog?.info('TcpConnector.connect endpoint=$host:$port', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.connect(host: host, port: port);
_debugLog?.info(
'TcpConnector.connect done, endpoint=${_service.activeEndpoint}',
tag: 'TCP',
);
}
StreamSubscription<Uint8List> listenFrames({
required void Function(Uint8List) onFrame,
required void Function(Object, StackTrace?) onError,
required void Function() onDone,
}) {
_frameSubscription = _service.frameStream.listen(
onFrame,
onError: onError,
onDone: onDone,
);
return _frameSubscription!;
}
Future<void> cancelFrameSubscription() async {
await _frameSubscription?.cancel();
_frameSubscription = null;
}
Future<void> disconnect() async {
if (!_service.isConnected && _frameSubscription == null) return;
_debugLog?.info('TcpConnector.disconnect', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.disconnect();
}
Future<void> write(Uint8List data) => _service.write(data);
void dispose() {
_frameSubscription?.cancel();
_service.dispose();
}
}
+78
View File
@@ -0,0 +1,78 @@
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/usb_serial_service.dart';
/// Manages USB serial transport for MeshCore devices.
///
/// Owns the [UsbSerialService] and USB-specific connection state.
/// The main [MeshCoreConnector] delegates all USB operations here.
class MeshCoreUsbManager {
MeshCoreUsbManager();
final UsbSerialService _service = UsbSerialService();
AppDebugLogService? _debugLog;
String? _activePortKey;
String? _activePortLabel;
// --- Getters ---
String? get activePortKey => _activePortKey;
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
bool get isConnected => _service.isConnected;
Stream<Uint8List> get frameStream => _service.frameStream;
// --- Configuration ---
Future<List<String>> listPorts() => _service.listPorts();
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
void setFallbackDeviceName(String label) =>
_service.setFallbackDeviceName(label);
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
_debugLog?.info(
'UsbManager.connect: portName=$portName baud=$baudRate',
tag: 'USB',
);
await _service.connect(portName: portName, baudRate: baudRate);
_activePortKey = _service.activePortKey ?? portName;
_activePortLabel = _service.activePortDisplayLabel ?? portName;
_debugLog?.info(
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
tag: 'USB',
);
}
Future<void> disconnect() async {
if (!_service.isConnected && _activePortKey == null) {
return;
}
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
await _service.disconnect();
_activePortKey = null;
_activePortLabel = null;
}
Future<void> write(Uint8List data) => _service.write(data);
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
// --- Label management ---
void updateConnectedLabel(String selfName) {
_service.updateConnectedLabel(selfName);
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
}
void dispose() {
_service.dispose();
}
}
+220 -103
View File
@@ -1,9 +1,12 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
// Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader {
int _pointer = 0;
int _lastPointer = 0;
final Uint8List _buffer;
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
@@ -13,6 +16,7 @@ class BufferReader {
int readByte() => readBytes(1)[0];
Uint8List readBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
@@ -24,6 +28,7 @@ class BufferReader {
}
void skipBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
@@ -34,10 +39,8 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readCString(int maxLength) {
String readCStringGreedy(int maxLength) {
_lastPointer = _pointer;
final value = <int>[];
final bytes = readBytes(maxLength);
for (final byte in bytes) {
@@ -51,6 +54,25 @@ class BufferReader {
}
}
String readCString({int maxLength = -1}) {
final backupPointer = _pointer;
final value = <int>[];
int counter = 0;
final maxLen = maxLength >= 0 ? maxLength : remaining;
while (counter < maxLen) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
counter++;
}
_lastPointer = backupPointer;
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() =>
@@ -72,6 +94,9 @@ class BufferReader {
if ((value & 0x800000) != 0) value -= 0x1000000;
return value;
}
void resetPointer() => _pointer = 0;
void rewind() => _pointer = _lastPointer;
}
// Buffer Writer - accumulating binary data builder
@@ -114,23 +139,38 @@ class BufferWriter {
}
void writeHex(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException(
'Invalid hex characters at position $i: $hexByte',
);
}
result.add(byte);
}
writeBytes(Uint8List.fromList(result));
writeBytes(hex2Uint8List(hex));
}
void writeBytesPadded(Uint8List bytes, int totalLength) {
// Path data (64 bytes, zero-padded)
final bytesPadded = Uint8List(totalLength);
final len = bytes.length < totalLength ? bytes.length : totalLength;
if (bytes.isNotEmpty && len > 0) {
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
for (int i = 0; i < copyLen; i++) {
bytesPadded[i] = bytes[i];
}
}
writeBytes(bytesPadded);
}
}
Uint8List hex2Uint8List(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException('Invalid hex characters at position $i: $hexByte');
}
result.add(byte);
}
return Uint8List.fromList(result);
}
// Command codes (to device)
@@ -162,15 +202,20 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
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;
// Text message types
const int txtTypePlain = 0;
const int txtTypeCliData = 1;
const int txtTypeSigned = 2;
// Repeater request types (for server requests)
const int reqTypeGetStatus = 0x01;
@@ -200,8 +245,13 @@ const int respCodeDeviceInfo = 13;
const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
const int respCodeAutoAddConfig = 25;
const int respCodeStats = 24;
const int statsTypeCore = 0;
const int statsTypeRadio = 1;
const int statsTypePackets = 2;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -223,6 +273,10 @@ const int advTypeRepeater = 2;
const int advTypeRoom = 3;
const int advTypeSensor = 4;
const int teleModeDeny = 0;
const int teleModeAllowFlags = 1; // use contact.flags
const int teleModeAllowAll = 2;
// Payload Types
const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
@@ -247,13 +301,26 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
const int payloadTypeRawCustom =
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
//auto-add flags
const int autoAddOverwriteOldestFlag =
1 << 0; // 0x01 - overwrite oldest non-favourite when full
const int autoAddChatFlag =
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
const int autoAddRepeaterFlag =
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
const int autoAddRoomServerFlag =
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
const int autoAddSensorFlag =
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
// Sizes
const int pubKeySize = 32;
const int signatureSize = 64;
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 =
@@ -291,13 +358,16 @@ const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33;
const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01;
const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
const int contactFlagTeleLoc = 0x04;
const int contactFlagTeleEnv = 0x08; //access environment sensors
const int contactPathLenOffset = 35;
const int contactPathOffset = 36;
const int contactNameOffset = 100;
const int contactTimestampOffset = 132;
const int contactLatOffset = 136;
const int contactLonOffset = 140;
const int contactLastmodOffset = 144;
const int contactLastModOffset = 144;
const int contactFrameSize = 148;
// Message frame offsets
@@ -309,52 +379,44 @@ const int msgTextOffset = 38;
class ParsedContactText {
final Uint8List senderPrefix;
final String text;
const ParsedContactText({required this.senderPrefix, required this.text});
}
ParsedContactText? parseContactMessageText(Uint8List frame) {
if (frame.isEmpty) return null;
final code = frame[0];
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
final message = BufferReader(frame);
try {
final code = message.readByte();
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
return null;
}
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
if (code == respCodeContactMsgRecvV3) {
// Skip SNR and reserved bytes in v3 layout
message.skipBytes(3);
}
final senderPrefix = message.readBytes(6); // public key
message.skipBytes(1); // path length
final textType = message.readByte();
message.skipBytes(4); // timestamp (4 bytes)
final shiftedType = textType >> 2;
final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
if (isSigned) {
// Signed messages have a 4-byte signature after the timestamp, before the text
message.skipBytes(4);
}
final text = message.readCString();
if (text.isEmpty) return null;
return ParsedContactText(senderPrefix: senderPrefix, text: text);
} catch (e) {
debugPrint('Error parsing contact message text: $e');
return null;
}
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
final isV3 = code == respCodeContactMsgRecvV3;
final prefixOffset = isV3 ? 4 : 1;
const prefixLen = 6;
final txtTypeOffset = prefixOffset + prefixLen + 1;
final timestampOffset = txtTypeOffset + 1;
final baseTextOffset = timestampOffset + 4;
if (frame.length <= baseTextOffset) return null;
final flags = frame[txtTypeOffset];
final shiftedType = flags >> 2;
final rawType = flags;
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
if (!isPlain && !isCli) {
return null;
}
var text = readCString(
frame,
baseTextOffset,
frame.length - baseTextOffset,
).trim();
if (text.isEmpty && frame.length > baseTextOffset + 4) {
text = readCString(
frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
).trim();
}
if (text.isEmpty) return null;
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
return ParsedContactText(senderPrefix: senderPrefix, text: text);
}
// Helper to read uint32 little-endian
@@ -377,18 +439,9 @@ int readInt32LE(Uint8List data, int offset) {
return val;
}
// Helper to read null-terminated UTF-8 string
String readCString(Uint8List data, int offset, int maxLen) {
int end = offset;
while (end < offset + maxLen && end < data.length && data[end] != 0) {
end++;
}
try {
return utf8.decode(data.sublist(offset, end), allowMalformed: true);
} catch (e) {
// Fallback to Latin-1 if UTF-8 decoding fails
return String.fromCharCodes(data.sublist(offset, end));
}
// Helper to convert uint32 to hex string
String ackHashToHex(int ackHash) {
return ackHash.toRadixString(16).padLeft(8, '0');
}
// Helper to convert public key to hex string
@@ -448,7 +501,7 @@ Uint8List buildSendTextMsgFrame(
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain);
writer.writeByte(attempt.clamp(0, 3));
writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
writer.writeBytes(recipientPubKey.sublist(0, 6));
writer.writeString(text);
@@ -508,6 +561,17 @@ Uint8List buildGetBattAndStorageFrame() {
return Uint8List.fromList([cmdGetBattAndStorage]);
}
/// Companion radio stats: [56][statsType] where statsType is statsTypeCore/Radio/Packets.
Uint8List buildGetStatsFrame(int statsType) {
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
}
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
Uint8List buildSetPathHashModeFrame(int mode) {
final m = mode.clamp(0, 2);
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
}
// Build CMD_SET_DEVICE_TIME frame
Uint8List buildSetDeviceTimeFrame(int timestamp) {
final writer = BufferWriter();
@@ -628,14 +692,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) {
}
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4]
Uint8List buildUpdateContactPathFrame(
Uint8List pubKey,
Uint8List customPath,
Uint8List path,
int pathLen, {
int type = 1, // ADV_TYPE_CHAT
int flags = 0,
String name = '',
double? lat,
double? lon,
DateTime? lastModified,
}) {
final writer = BufferWriter();
writer.writeByte(cmdAddUpdateContact);
@@ -644,17 +711,7 @@ Uint8List buildUpdateContactPathFrame(
writer.writeByte(flags);
writer.writeByte(pathLen);
// Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i];
}
}
writer.writeBytes(pathPadded);
writer.writeBytesPadded(path, maxPathSize);
// Name (32 bytes, null-padded)
writer.writeCString(name, maxNameSize);
@@ -663,6 +720,32 @@ Uint8List buildUpdateContactPathFrame(
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(timestamp);
if ((lat == null || lon == null) && lastModified != null) {
// If lat/lon not provided, write zeros
writer.writeInt32LE(0);
writer.writeInt32LE(0);
} else {
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
// Latitude
final latitude = lat ?? 0.0;
writer.writeInt32LE((latitude * 1e6).round());
// Longitude
final longitude = lon ?? 0.0;
writer.writeInt32LE((longitude * 1e6).round());
}
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();
}
@@ -675,16 +758,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
return writer.toBytes();
}
// Build CMD_GET_RADIO_SETTINGS frame
Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]);
}
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
Uint8List buildGetAutoAddFlagsFrame() {
return Uint8List.fromList([cmdGetAutoAddConfig]);
}
// Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds
@@ -764,7 +846,7 @@ Uint8List buildSendCliCommandFrame(
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData);
writer.writeByte(attempt.clamp(0, 3));
writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
writer.writeBytes(repeaterPubKey.sublist(0, 6));
writer.writeString(command);
@@ -809,10 +891,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(String contactFrame) {
Uint8List buildImportContactFrame(Uint8List contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeHex(contactFrame);
writer.writeBytes(contactFrame);
return writer.toBytes();
}
@@ -826,20 +908,55 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
Uint8List buildSetOtherParamsFrame(
bool allowAutoAddContacts,
int allowTelemetryFlags,
int advertLocationPolicy,
int multiAcks,
) {
final writer = BufferWriter();
writer.writeByte(cmdSetOtherParams);
writer.writeByte(
allowAutoAddContacts ? 0x00 : 0x01,
); // Allow Auto Add Contacts
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
writer.writeByte(0x01);
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
writer.writeByte(multiAcks); // Multi Acknowledgements
return writer.toBytes();
}
// Build CMD_SET_AUTO_ADD_CONFIG frame
// Format: [cmd][flags]
Uint8List buildSetAutoAddConfigFrame({
required bool autoAddChat,
required bool autoAddRepeater,
required bool autoAddRoomServer,
required bool autoAddSensor,
required bool overwriteOldest,
}) {
final writer = BufferWriter();
writer.writeByte(cmdSetAutoAddConfig);
int flags = 0;
if (autoAddChat) flags |= autoAddChatFlag;
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
if (autoAddSensor) flags |= autoAddSensorFlag;
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
writer.writeByte(flags);
return writer.toBytes();
}
//Build CMD_SEND_TELEMETRY_REQ
// Format: [cmd][reserved x3][pub_key? x32]
Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdSendTelemetryReq);
if (pubKey != null && pubKey.length == pubKeySize) {
writer.writeBytes(Uint8List(3)); // reserved bytes
writer.writeBytes(pubKey);
} else {
writer.writeBytes(Uint8List(4)); // reserved bytes
}
return writer.toBytes();
}
+15
View File
@@ -0,0 +1,15 @@
class MeshCoreUuids {
static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
static const List<String> deviceNamePrefixes = [
"MeshCore-",
"Whisper-",
"WisCore-",
"Seeed",
"Lilygo",
"HT-",
"LowMesh_MC_",
];
}
+19
View File
@@ -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) {
+38
View File
@@ -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';
}
}
+51 -10
View File
@@ -1,8 +1,51 @@
import 'package:flutter/material.dart';
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) {
final brightness = Theme.of(context).brightness;
final orange = brightness == Brightness.dark
? const Color(0xFFFFB74D)
: const Color(0xFFE65100);
return base.copyWith(color: orange, decoration: TextDecoration.underline);
}
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
static Widget buildLinkifyText({
required BuildContext context,
required String text,
required TextStyle style,
TextStyle? linkStyle,
}) {
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) {
return SelectableLinkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
return Linkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
static Future<void> handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog
final shouldOpen = await showDialog<bool>(
@@ -51,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,
);
}
}
+31
View File
@@ -0,0 +1,31 @@
import '../models/contact.dart';
import '../connector/meshcore_protocol.dart';
class PathHelper {
static String formatPathHex(List<int> pathBytes) {
return pathBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(',');
}
static String resolvePathNames(
List<int> pathBytes,
List<Contact> allContacts,
) {
return pathBytes
.map((b) {
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
final matches = allContacts
.where(
(c) =>
c.publicKey.first == b &&
(c.type == advTypeRepeater || c.type == advTypeRoom),
)
.toList();
if (matches.isEmpty) return hex;
if (matches.length == 1) return matches.first.name;
return matches.map((c) => c.name).join(' | ');
})
.join(' \u2192 ');
}
}
+49
View File
@@ -8,6 +8,50 @@ class ReactionInfo {
}
class ReactionHelper {
/// Apply a reaction to a list of messages by matching the reaction hash.
///
/// [messages] - the message list to search
/// [reactionInfo] - the parsed reaction
/// [getTimestampSecs] - extract timestamp seconds from a message
/// [getSenderName] - extract sender name for hash (null for 1:1 implicit)
/// [getMessageText] - extract message text
/// [getReactions] - extract current reactions map
/// [shouldSkip] - filter function to skip messages (e.g., skip outgoing for incoming reactions)
/// [updateMessage] - callback to update the message at index with new reactions
///
/// Returns whether a match was found.
static bool applyReaction<T>({
required List<T> messages,
required ReactionInfo reactionInfo,
required int Function(T) getTimestampSecs,
required String? Function(T) getSenderName,
required String Function(T) getMessageText,
required Map<String, int> Function(T) getReactions,
required bool Function(T) shouldSkip,
required void Function(int index, Map<String, int> newReactions)
updateMessage,
}) {
final targetHash = reactionInfo.targetHash;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
if (shouldSkip(msg)) continue;
final msgHash = computeReactionHash(
getTimestampSecs(msg),
getSenderName(msg),
getMessageText(msg),
);
if (msgHash == targetHash) {
final currentReactions = Map<String, int>.from(getReactions(msg));
currentReactions[reactionInfo.emoji] =
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
updateMessage(i, currentReactions);
return true;
}
}
return false;
}
static List<String>? _cachedEmojis;
/// Combined list of all reaction emojis in fixed order.
@@ -65,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';
}
}
+56
View File
@@ -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,
),
);
}
+16 -3
View File
@@ -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) {
+351 -1
View File
@@ -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": "Реклама изпратена",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Нова група",
"contacts_groupName": "Група",
"contacts_groupNameRequired": "Името на групата е задължително.",
"contacts_groupNameReserved": "Това име на група е запазено",
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1606,6 +1609,8 @@
"scanner_bluetoothOff": "Bluetooth е изключен.",
"scanner_enableBluetooth": "Активирайте Bluetooth",
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
"scanner_chromeRequired": "Изисква се браузър Chrome",
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
"snrIndicator_lastSeen": "Последно видян",
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
"chat_ShowAllPaths": "Покажи всички пътища",
@@ -1799,5 +1804,350 @@
"contacts_unread": "Непрочетено",
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
"contacts_searchContactsNoNumber": "Търси контакти...",
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
"contacts_searchUsers": "Търсене на {number}{str} потребители...",
"contactsSettings_title": "Настройки на контактите",
"contactsSettings_autoAddTitle": "Автоматично откриване",
"contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители",
"contactsSettings_otherTitle": "Други настройки свързани с контакти",
"settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.",
"settings_contactSettings": "Настройки за контакти",
"contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици",
"contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите",
"contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.",
"contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи",
"contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.",
"contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.",
"contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.",
"contactsSettings_overwriteOldestTitle": "Премахни най-старото",
"discoveredContacts_Title": "Открити контакти",
"discoveredContacts_searchHint": "Търсене на открити контакти",
"discoveredContacts_noMatching": "Няма съвпадащи контакти",
"discoveredContacts_contactAdded": "Контакт добавен",
"discoveredContacts_copyContact": "Копирай контакт в клипборда",
"discoveredContacts_deleteContact": "Изтрий контакт",
"discoveredContacts_addContact": "Добави контакт",
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
"common_deleteAll": "Изтрий всичко",
"map_guessedLocation": "Предполагано местоположение",
"map_showGuessedLocations": "Покажете местоположенията на предположените възли.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Свържете се чрез USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.",
"usbScreenStatus": "Изберете USB устройство",
"usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.",
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.",
"usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.",
"usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.",
"usbErrorInvalidPort": "Изберете валитно USB устройство.",
"usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.",
"usbErrorNotConnected": "Няма свързано USB устройство.",
"usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.",
"usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.",
"usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.",
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
"usbErrorPortClosed": "USB връзката не е активна.",
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Свързване към USB устройство...",
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
"usbStatus_notConnected": "Изберете USB устройство",
"usbStatus_searching": "Търсене на USB устройства...",
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Свържете се чрез TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP адрес",
"tcpPortLabel": "Пристанище",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Въведете крайната точка и свържете се.",
"tcpStatus_connectingTo": "Свързване към {endpoint}...",
"tcpErrorHostRequired": "Необходим е IP адрес.",
"tcpErrorPortInvalid": "Портът трябва да бъде между 1 и 65535.",
"tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.",
"tcpErrorTimedOut": "Връзката TCP изтекла.",
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
"map_showDiscoveryContacts": "Покажи контакти за откриване",
"map_setAsMyLocation": "Задайте като моя местоположение",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_denyAll": "Откажи всичко",
"settings_allowAll": "Позволи всичко",
"settings_allowByContact": "Позволи по флагове за контакт",
"settings_privacy": "Настройки на поверителността",
"settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.",
"settings_privacySubtitle": "Контролирайте каква информация се споделя.",
"settings_telemetryBaseMode": "Базов режим на телеметрия",
"settings_telemetryLocationMode": "Режим на местоположение на телеметрията",
"settings_advertLocation": "Място на обявата",
"settings_advertLocationSubtitle": "Включи местоположение в обявата",
"contact_info": "Контактна информация",
"settings_telemetryEnvironmentMode": "Режим на средата на телеметрията",
"contact_telemetry": "Телеметрия",
"contact_lastSeen": "Последно видян",
"contact_clearChat": "Изчисти чата",
"contact_teleBase": "Базата данни за телеметрия",
"contact_settings": "Настройки за контакти",
"contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия",
"contact_teleEnv": "Среда на телеметрия",
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
"contact_teleLoc": "Местоположение на телеметрията",
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
"appSettings_maxRouteWeightSubtitle": "Максималното тегло, което един маршрут може да събере от успешни доставки.",
"appSettings_routeWeightSuccessIncrement": "Увеличение на теглото за успех",
"appSettings_routeWeightSuccessIncrementSubtitle": "Тегло, добавено към път след успешно доставяне.",
"appSettings_routeWeightFailureDecrement": "Намаляване на теглото, свързано с неуспех",
"appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.",
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
"map_showOverlaps": "Покриване на ключа на повтаряча",
"map_runTraceWithReturnPath": "Върни се по същия път.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Моля, изчакайте малко, преди да изпратите отново.",
"appSettings_languageHu": "Унгарски",
"appSettings_jumpToOldestUnread": "Преминете към най-старата непочетена статия",
"appSettings_jumpToOldestUnreadSubtitle": "Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.",
"appSettings_languageJa": "Японски",
"appSettings_languageKo": "Корейски",
"radioStats_tooltip": "Статистика за радио и мрежа",
"radioStats_screenTitle": "Статистически данни за радиопредаванията",
"radioStats_notConnected": "Свържете се с устройство, за да видите статистически данни за радиопредаване.",
"radioStats_firmwareTooOld": "Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.",
"radioStats_waiting": "Изчакване на данни…",
"radioStats_noiseFloor": "Ниво на шума: {noiseDbm} dBm",
"radioStats_lastRssi": "Последен RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Последна стойност на SNR: {snr} dB",
"radioStats_txAir": "Време на въздух (общо): {seconds} секунди",
"radioStats_rxAir": "Общо време на използване на RX (в секунди): {seconds} с",
"radioStats_chartCaption": "Ниво на шума (dBm) за последните измервания.",
"radioStats_stripNoise": "Ниво на шума: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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",
"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})."
}
+357 -7
View File
@@ -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",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Neue Gruppe",
"contacts_groupName": "Gruppenname",
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
"contacts_groupNameReserved": "Dieser Gruppenname ist reserviert",
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -296,8 +299,8 @@
"contacts_filterContacts": "Filtert Kontakte...",
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
"contacts_noMembers": "Keine Mitglieder",
"contacts_lastSeenNow": "gerade gesehen",
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
"contacts_lastSeenNow": "kürzlich",
"contacts_lastSeenMinsAgo": "~ {minutes} Min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +308,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
"contacts_lastSeenHourAgo": "~ 1 Std.",
"contacts_lastSeenHoursAgo": "~ {hours} Std.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +317,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag",
"contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor",
"contacts_lastSeenDayAgo": "~ 1 Tag",
"contacts_lastSeenDaysAgo": "~ {days} Tage",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1635,6 +1638,8 @@
"pathTrace_clearTooltip": "Pfad löschen",
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
"scanner_chromeRequired": "Chrome Browser erforderlich",
"scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.",
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
"scanner_enableBluetooth": "Bluetooth aktivieren",
"snrIndicator_lastSeen": "Zuletzt gesehen",
@@ -1827,5 +1832,350 @@
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
"contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
"settings_contactSettings": "Kontakteinstellungen",
"contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten",
"contactsSettings_title": "Kontakteinstellungen",
"contactsSettings_autoAddTitle": "Automatische Erkennung",
"contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern",
"settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten",
"contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen",
"contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen",
"contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen",
"contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen",
"contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen",
"contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.",
"discoveredContacts_noMatching": "Keine passenden Kontakte",
"discoveredContacts_searchHint": "Entdeckte Kontakte suchen",
"discoveredContacts_addContact": "Kontakt hinzufügen",
"discoveredContacts_contactAdded": "Kontakt hinzugefügt",
"discoveredContacts_deleteContact": "Kontakt löschen",
"discoveredContacts_Title": "Entdeckte Kontakte",
"discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren",
"contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten",
"contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen",
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
"common_deleteAll": "Alles löschen",
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
"map_guessedLocation": "Geschätzter Ort",
"usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Verbinden über USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Wählen Sie ein USB-Gerät aus",
"usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.",
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.",
"usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.",
"usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.",
"usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.",
"usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.",
"usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.",
"usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.",
"usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.",
"usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.",
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
"usbFallbackDeviceName": "Web-Serielle Geräte",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Suche nach USB-Geräten...",
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "IP-Adresse",
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Verbinden über TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.",
"tcpStatus_connectingTo": "Verbindung zu {endpoint}...",
"tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.",
"tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.",
"tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.",
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
"map_setAsMyLocation": "Als meine aktuelle Position festlegen",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_allowByContact": "Zulassen durch Kontaktflaggen",
"settings_privacy": "Datenschutzeinstellungen",
"settings_allowAll": "Alles zulassen",
"settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.",
"settings_denyAll": "Alle ablehnen",
"settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.",
"settings_telemetryLocationMode": "Telemetrie-Ortsmodus",
"settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus",
"settings_advertLocation": "Anzeigenort",
"settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen",
"settings_telemetryBaseMode": "Telemetrie-Basismodus",
"contact_teleBase": "Telemetriebasis",
"contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie",
"contact_teleLoc": "Telemetrieort",
"contact_teleLocSubtitle": "Teilen von Standortdaten zulassen",
"contact_info": "Kontaktinformationen",
"contact_settings": "Kontakteinstellungen",
"contact_telemetry": "Telemetrie",
"contact_teleEnv": "Telemetrieumgebung",
"contact_lastSeen": "Zuletzt gesehen",
"contact_clearChat": "Chat löschen",
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
"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",
"appSettings_initialRouteWeight": "Anfangs-Streckengewicht",
"appSettings_routeWeightSuccessIncrement": "Erhöhung des Erfolgsgewichts",
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.",
"appSettings_routeWeightFailureDecrement": "Reduzierung des Gewichts bei Fehlern",
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde",
"appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen",
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"map_showOverlaps": "Überlappungen der Repeater-Taste",
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Bitte warten Sie einen Moment, bevor Sie erneut senden.",
"appSettings_jumpToOldestUnread": "Zum ältesten, nicht gelesenen Eintrag springen",
"appSettings_languageHu": "Ungarisch",
"appSettings_jumpToOldestUnreadSubtitle": "Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.",
"appSettings_languageJa": "Japanisch",
"appSettings_languageKo": "Koreanisch",
"radioStats_tooltip": "Daten zu Radio- und Mesh-Netzwerken",
"radioStats_screenTitle": "Senderinformationen",
"radioStats_notConnected": "Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.",
"radioStats_firmwareTooOld": "Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.",
"radioStats_waiting": "Warte auf Daten…",
"radioStats_noiseFloor": "Rauschpegel: {noiseDbm} dBm",
"radioStats_lastRssi": "Letzter RSSI-Wert: {rssiDbm} dBm",
"radioStats_lastSnr": "Letzter SNR: {snr} dB",
"radioStats_txAir": "Gesamt-TX-Zeit: {seconds} s",
"radioStats_rxAir": "Gesamt-RX-Zeit: {seconds} s",
"radioStats_chartCaption": "Rauschpegel (dBm) basierend auf den letzten Messwerten.",
"radioStats_stripNoise": "Rauschpegel: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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).",
"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})."
}
+381 -12
View File
@@ -10,7 +10,9 @@
"common_unknownDevice": "Unknown Device",
"common_save": "Save",
"common_delete": "Delete",
"common_deleteAll": "Delete All",
"common_close": "Close",
"common_done": "Done",
"common_edit": "Edit",
"common_add": "Add",
"common_settings": "Settings",
@@ -46,6 +48,64 @@
}
},
"scanner_title": "MeshCore Open",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Connect over TCP",
"tcpHostLabel": "IP Address",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Enter endpoint and connect",
"tcpStatus_connectingTo": "Connecting to {endpoint}...",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"tcpErrorHostRequired": "IP address is required.",
"tcpErrorPortInvalid": "Port must be between 1 and 65535.",
"tcpErrorUnsupported": "TCP transport is not supported on this platform.",
"tcpErrorTimedOut": "TCP connection timed out.",
"tcpConnectionFailed": "TCP connection failed: {error}",
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbScreenTitle": "Connect over USB",
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
"usbScreenStatus": "Select a USB device",
"usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
"usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
"usbErrorPermissionDenied": "USB permission was denied.",
"usbErrorDeviceMissing": "The selected USB device is no longer available.",
"usbErrorInvalidPort": "Select a valid USB device.",
"usbErrorBusy": "Another USB connection request is already in progress.",
"usbErrorNotConnected": "No USB device is connected.",
"usbErrorOpenFailed": "Failed to open the selected USB device.",
"usbErrorConnectFailed": "Failed to connect to the selected USB device.",
"usbErrorUnsupported": "USB serial is not supported on this platform.",
"usbErrorAlreadyActive": "A USB connection is already active.",
"usbErrorNoDeviceSelected": "No USB device was selected.",
"usbErrorPortClosed": "The USB connection is not open.",
"usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.",
"usbFallbackDeviceName": "Web Serial Device",
"usbStatus_notConnected": "Select a USB device",
"usbStatus_connecting": "Connecting to USB device...",
"usbStatus_searching": "Searching for USB devices...",
"usbConnectionFailed": "USB connection failed: {error}",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"scanner_scanning": "Scanning for devices...",
"scanner_connecting": "Connecting...",
"scanner_disconnecting": "Disconnecting...",
@@ -68,10 +128,13 @@
}
}
},
"scanner_stop": "Stop",
"scanner_scan": "Scan",
"scanner_bluetoothOff": "Bluetooth is off",
"scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices",
"scanner_chromeRequired": "Chrome Browser Required",
"scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.",
"scanner_enableBluetooth": "Enable Bluetooth",
"device_quickSwitch": "Quick switch",
"device_meshcore": "MeshCore",
@@ -98,12 +161,29 @@
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_contactSettings": "Contact Settings",
"settings_contactSettingsSubtitle": "Settings for how contacts are added.",
"settings_privacyMode": "Privacy Mode",
"settings_privacyModeSubtitle": "Hide name/location in advertisements",
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
"settings_privacyModeEnabled": "Privacy mode enabled",
"settings_privacyModeDisabled": "Privacy mode disabled",
"settings_privacy": "Privacy Settings",
"settings_privacySubtitle": "Control what information is shared.",
"settings_privacySettingsDescription": "Choose what information your device shares with others.",
"settings_denyAll": "Deny all",
"settings_allowByContact": "Allow by contact flags",
"settings_allowAll": "Allow all",
"settings_telemetryBaseMode": "Telemetry Base Mode",
"settings_telemetryLocationMode": "Telemetry Location Mode",
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.",
"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",
@@ -116,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",
@@ -206,6 +286,27 @@
"appSettings_autoRouteRotationSubtitle": "Cycle between best paths and flood mode",
"appSettings_autoRouteRotationEnabled": "Auto route rotation enabled",
"appSettings_autoRouteRotationDisabled": "Auto route rotation disabled",
"appSettings_maxRouteWeight": "Max Route Weight",
"appSettings_maxRouteWeightSubtitle": "Maximum weight a path can accumulate from successful deliveries",
"appSettings_initialRouteWeight": "Initial Route Weight",
"appSettings_initialRouteWeightSubtitle": "Starting weight for newly discovered paths",
"appSettings_routeWeightSuccessIncrement": "Success Weight Increment",
"appSettings_routeWeightSuccessIncrementSubtitle": "Weight added to a path after successful delivery",
"appSettings_routeWeightFailureDecrement": "Failure Weight Decrement",
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
"appSettings_maxMessageRetries": "Max Message Retries",
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
"path_routeWeight": "{weight}/{max}",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"appSettings_battery": "Battery",
"appSettings_batteryChemistry": "Battery Chemistry",
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
@@ -353,6 +454,7 @@
"contacts_newGroup": "New Group",
"contacts_groupName": "Group name",
"contacts_groupNameRequired": "Group name is required",
"contacts_groupNameReserved": "This group name is reserved",
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -364,8 +466,8 @@
"contacts_filterContacts": "Filter contacts...",
"contacts_noContactsMatchFilter": "No contacts match your filter",
"contacts_noMembers": "No members",
"contacts_lastSeenNow": "Last seen now",
"contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago",
"contacts_lastSeenNow": "recently",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -373,8 +475,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Last seen 1 hour ago",
"contacts_lastSeenHoursAgo": "Last seen {hours} hours ago",
"contacts_lastSeenHourAgo": "~ 1 hour",
"contacts_lastSeenHoursAgo": "~ {hours} hours",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -382,8 +484,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Last seen 1 day ago",
"contacts_lastSeenDaysAgo": "Last seen {days} days ago",
"contacts_lastSeenDayAgo": "~ 1 day",
"contacts_lastSeenDaysAgo": "~ {days} days",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -391,6 +493,17 @@
}
}
},
"contact_info": "Contact Info",
"contact_settings": "Contact Settings",
"contact_telemetry": "Telemetry",
"contact_lastSeen": "Last seen",
"contact_clearChat": "Clear Chat",
"contact_teleBase": "Telemetry Base",
"contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry",
"contact_teleLoc": "Telemetry Location",
"contact_teleLocSubtitle": "Allow sharing location data",
"contact_teleEnv": "Telemetry Environment",
"contact_teleEnvSubtitle": "Allow sharing environment sensor data",
"channels_title": "Channels",
"channels_noChannelsConfigured": "No channels configured",
"channels_addPublicChannel": "Add Public Channel",
@@ -406,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",
@@ -490,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}",
@@ -647,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:",
@@ -698,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",
@@ -743,7 +876,14 @@
"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",
"map_label": "Label",
"map_pointOfInterest": "Point of interest",
@@ -765,17 +905,23 @@
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Other Nodes",
"map_showOverlaps": "Repeater Key Overlaps",
"map_keyPrefix": "Key Prefix",
"map_filterByKeyPrefix": "Filter by key prefix",
"map_publicKeyPrefix": "Public key prefix",
"map_markers": "Markers",
"map_showSharedMarkers": "Show shared markers",
"map_showGuessedLocations": "Show guessed node locations",
"map_showDiscoveryContacts": "Show Discovery Contacts",
"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.",
"map_runTrace": "Run Path Trace",
"map_runTrace": "Run path trace",
"map_runTraceWithReturnPath": "Return back on the same path.",
"map_removeLast": "Remove Last",
"map_pathTraceCancelled": "Path trace cancelled.",
"mapCache_title": "Offline Map Cache",
@@ -906,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)",
@@ -973,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",
@@ -984,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)",
@@ -1214,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.",
@@ -1737,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",
@@ -1837,5 +2036,175 @@
"settings_gpxExportShareText": "Map data exported from meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open GPX map data export",
"snrIndicator_nearByRepeaters": "Nearby Repeaters",
"snrIndicator_lastSeen": "Last seen"
"snrIndicator_lastSeen": "Last seen",
"contactsSettings_title": "Contacts settings",
"contactsSettings_autoAddTitle": "Automatic Discovery",
"contactsSettings_otherTitle": "Other contact related settings",
"contactsSettings_autoAddUsersTitle": "Auto-add users",
"contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.",
"contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters",
"contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.",
"contactsSettings_autoAddRoomServersTitle": "Auto-add room servers",
"contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.",
"contactsSettings_autoAddSensorsTitle": "Auto-add sensors",
"contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.",
"contactsSettings_overwriteOldestTitle": "Overwrite Oldest",
"contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.",
"discoveredContacts_Title": "Discovered Contacts",
"discoveredContacts_noMatching": "No matching contacts",
"discoveredContacts_searchHint": "Search discovered contacts",
"discoveredContacts_contactAdded": "Contact added",
"discoveredContacts_addContact": "Add Contact",
"discoveredContacts_copyContact": "Copy Contact to clipboard",
"discoveredContacts_deleteContact": "Delete Discovered Contact",
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?",
"chat_sendCooldown": "Please wait a moment before sending again.",
"appSettings_jumpToOldestUnread": "Jump to oldest unread",
"appSettings_jumpToOldestUnreadSubtitle": "When opening a chat with unread messages, scroll to the first unread instead of the latest.",
"appSettings_languageHu": "Hungarian",
"appSettings_languageJa": "Japanese",
"appSettings_languageKo": "Korean",
"radioStats_tooltip": "Radio & mesh stats",
"radioStats_screenTitle": "Radio stats",
"radioStats_notConnected": "Connect to a device to view radio statistics.",
"radioStats_firmwareTooOld": "Radio statistics require companion firmware v8 or newer.",
"radioStats_waiting": "Waiting for data…",
"radioStats_noiseFloor": "Noise floor: {noiseDbm} dBm",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"radioStats_lastRssi": "Last RSSI: {rssiDbm} dBm",
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"radioStats_lastSnr": "Last SNR: {snr} dB",
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"radioStats_txAir": "TX airtime (total): {seconds} s",
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"radioStats_rxAir": "RX airtime (total): {seconds} s",
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"radioStats_chartCaption": "Noise floor (dBm) over recent samples.",
"radioStats_stripNoise": "Noise floor: {noiseDbm} dBm",
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"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",
"scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"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"
}
+356 -6
View File
@@ -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",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Nuevo Grupo",
"contacts_groupName": "Nombre del grupo",
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
"contacts_groupNameReserved": "Este nombre de grupo está reservado",
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -297,7 +300,7 @@
"contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro",
"contacts_noMembers": "No miembros",
"contacts_lastSeenNow": "Última vez que se vio ahora",
"contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +308,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora",
"contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.",
"contacts_lastSeenHourAgo": "~ 1 hora",
"contacts_lastSeenHoursAgo": "~ {hours} horas",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +317,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día",
"contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.",
"contacts_lastSeenDayAgo": "~ 1 día",
"contacts_lastSeenDaysAgo": "~ {days} días",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1632,6 +1635,8 @@
"map_removeLast": "Eliminar último",
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
"scanner_chromeRequired": "Navegador Chrome requerido",
"scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.",
"scanner_bluetoothOff": "Bluetooth está desactivado.",
"scanner_enableBluetooth": "Habilitar Bluetooth",
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
@@ -1827,5 +1832,350 @@
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
"contactsSettings_autoAddTitle": "Detección automática",
"settings_contactSettings": "Configuración de contacto",
"contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente",
"contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto",
"contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.",
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.",
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.",
"contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente",
"contactsSettings_title": "Configuración de contactos",
"settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.",
"contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.",
"contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente",
"contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo",
"contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala",
"discoveredContacts_noMatching": "No se encontraron contactos coincidentes",
"discoveredContacts_contactAdded": "Contacto agregado",
"discoveredContacts_copyContact": "Copiar contacto al portapapeles",
"discoveredContacts_deleteContact": "Eliminar contacto",
"discoveredContacts_Title": "Contactos descubiertos",
"discoveredContacts_searchHint": "Buscar contactos descubiertos",
"discoveredContacts_addContact": "Agregar contacto",
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
"common_deleteAll": "Eliminar todo",
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
"map_guessedLocation": "Ubicación estimada",
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.",
"usbScreenTitle": "Conecte mediante USB",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.",
"usbScreenStatus": "Seleccione un dispositivo USB",
"usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.",
"usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.",
"usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.",
"usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.",
"usbErrorInvalidPort": "Seleccione un dispositivo USB válido.",
"usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.",
"usbErrorNotConnected": "No hay ningún dispositivo USB conectado.",
"usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.",
"usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.",
"usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.",
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
"usbErrorPortClosed": "La conexión USB no está activa.",
"usbFallbackDeviceName": "Dispositivo de serie web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Conectándose al dispositivo USB...",
"usbStatus_searching": "Buscando dispositivos USB...",
"usbStatus_notConnected": "Seleccione un dispositivo USB",
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpScreenTitle": "Establecer conexión a través de TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "Dirección IP",
"tcpPortLabel": "Puerto",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Ingrese la dirección final y conecte.",
"tcpStatus_connectingTo": "Conectándose a {endpoint}...",
"tcpErrorHostRequired": "Se requiere la dirección IP.",
"tcpErrorPortInvalid": "El puerto debe estar entre 1 y 65535.",
"tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.",
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
"map_setAsMyLocation": "Establecer mi ubicación",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySubtitle": "Controlar qué información se comparte.",
"settings_allowByContact": "Permitir por banderas de contacto",
"settings_denyAll": "Denegar todo",
"settings_telemetryBaseMode": "Modo base de telemetría",
"settings_telemetryEnvironmentMode": "Modo de entorno de telemetría",
"settings_advertLocationSubtitle": "Incluir ubicación en anuncio",
"contact_info": "Información de contacto",
"settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.",
"settings_allowAll": "Permitir todo",
"settings_privacy": "Configuración de privacidad",
"contact_settings": "Configuración de contacto",
"settings_telemetryLocationMode": "Modo de ubicación de telemetría",
"contact_teleBase": "Base de Telemetría",
"contact_teleLoc": "Ubicación de telemetría",
"settings_advertLocation": "Ubicación de anuncio",
"contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación",
"contact_clearChat": "Borrar chat",
"contact_telemetry": "Telemetría",
"contact_lastSeen": "Visto por última vez",
"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",
"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",
"appSettings_maxRouteWeightSubtitle": "Peso máximo que una ruta puede acumular gracias a entregas exitosas.",
"appSettings_routeWeightSuccessIncrement": "Incremento de peso para el éxito",
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso añadido a una ruta después de una entrega exitosa.",
"appSettings_routeWeightFailureDecrement": "Reducción del peso asociado al fallo",
"appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.",
"appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes",
"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",
"map_showOverlaps": "Superposiciones de tecla repetidora",
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Salta a los mensajes más antiguos sin leer",
"chat_sendCooldown": "Por favor, espere un momento antes de reenviar.",
"appSettings_languageHu": "Húngaro",
"appSettings_jumpToOldestUnreadSubtitle": "Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.",
"appSettings_languageJa": "Japonés",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Estadísticas de radio y malla",
"radioStats_screenTitle": "Estadísticas de radio",
"radioStats_notConnected": "Conéctese a un dispositivo para visualizar estadísticas de radio.",
"radioStats_firmwareTooOld": "Las estadísticas de radio requieren un firmware compatible v8 o posterior.",
"radioStats_waiting": "Esperando datos…",
"radioStats_noiseFloor": "Nivel de ruido: {noiseDbm} dBm",
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Último SNR: {snr} dB",
"radioStats_txAir": "Tiempo de emisión en Texas (total): {seconds} s",
"radioStats_rxAir": "Tiempo de transmisión de RX (total): {seconds} s",
"radioStats_chartCaption": "Nivel de ruido (dBm) en muestras recientes.",
"radioStats_stripNoise": "Nivel de ruido: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"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",
"@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}"
}
+377 -27
View File
@@ -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)",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Nouveau Groupe",
"contacts_groupName": "Nom du groupe",
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
"contacts_groupNameReserved": "Ce nom de groupe est réservé",
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -297,7 +300,7 @@
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
"contacts_noMembers": "Aucun membre",
"contacts_lastSeenNow": "Vu maintenant",
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +308,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
"contacts_lastSeenHourAgo": "~ 1 heure",
"contacts_lastSeenHoursAgo": "~ {hours} heures",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +317,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
"contacts_lastSeenDayAgo": "~ 1 jour",
"contacts_lastSeenDaysAgo": "~ {days} jours",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -566,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.",
@@ -642,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é)",
@@ -681,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",
@@ -864,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": {
@@ -995,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",
@@ -1376,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",
@@ -1465,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é",
@@ -1477,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}\" ?",
@@ -1533,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": {
@@ -1553,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",
@@ -1582,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.",
@@ -1604,6 +1607,8 @@
"map_removeLast": "Supprimer le dernier",
"map_runTrace": "Exécuter la traçage de chemin",
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
"scanner_chromeRequired": "Navigateur Chrome requis",
"scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.",
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
"scanner_enableBluetooth": "Activer le Bluetooth",
"snrIndicator_lastSeen": "Dernière fois vu",
@@ -1797,7 +1802,352 @@
"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..."
"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 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",
"contactsSettings_autoAddTitle": "Découverte automatique",
"contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs",
"contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts",
"discoveredContacts_noMatching": "Aucun contact correspondant",
"discoveredContacts_contactAdded": "Contact ajouté",
"discoveredContacts_addContact": "Ajouter un contact",
"discoveredContacts_copyContact": "Copier le contact dans le presse-papiers",
"discoveredContacts_deleteContact": "Supprimer le contact",
"contactsSettings_overwriteOldestTitle": "Écraser le plus ancien",
"contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.",
"discoveredContacts_Title": "Contacts découverts",
"discoveredContacts_searchHint": "Rechercher des contacts découverts",
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
"common_deleteAll": "Supprimer tout",
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
"map_guessedLocation": "Lieu deviné",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Connectez via USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.",
"usbScreenStatus": "Sélectionnez un périphérique USB",
"usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.",
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.",
"usbErrorPermissionDenied": "L'accès via USB a été refusé.",
"usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.",
"usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.",
"usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.",
"usbErrorNotConnected": "Aucun appareil USB n'est connecté.",
"usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.",
"usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.",
"usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.",
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
"usbFallbackDeviceName": "Dispositif de communication série sur le Web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_notConnected": "Sélectionnez un périphérique USB",
"usbConnectionFailed": "Échec de la connexion USB : {error}",
"usbStatus_connecting": "Connexion au périphérique USB...",
"usbStatus_searching": "Recherche de périphériques USB...",
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Adresse IP",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Établir une connexion via TCP",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Entrez l'adresse de destination et connectez-vous.",
"tcpStatus_connectingTo": "Connexion à {endpoint}...",
"tcpErrorHostRequired": "Une adresse IP est obligatoire.",
"tcpErrorPortInvalid": "La taille du port doit être comprise entre 1 et 65535.",
"tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.",
"tcpErrorTimedOut": "La connexion TCP a expiré.",
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
"map_showDiscoveryContacts": "Afficher les contacts de découverte",
"map_setAsMyLocation": "Définir comme ma localisation",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Paramètres de confidentialité",
"settings_privacySubtitle": "Contrôlez les informations partagées",
"settings_telemetryLocationMode": "Mode d'emplacement de télémétrie",
"settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie",
"settings_advertLocation": "Emplacement de l'annonce",
"settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce",
"settings_denyAll": "Refuser tout",
"settings_allowByContact": "Autoriser par drapeaux de contact",
"settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.",
"settings_allowAll": "Autoriser tout",
"contact_info": "Informations de contact",
"settings_telemetryBaseMode": "Mode de base Télémétrie",
"contact_teleBase": "Base de télémétrie",
"contact_teleLoc": "Emplacement de télémétrie",
"contact_teleLocSubtitle": "Autoriser le partage des données de localisation",
"contact_teleEnv": "Environnement Télémétrie",
"contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement",
"contact_telemetry": "Télémétrie",
"contact_settings": "Paramètres de contact",
"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",
"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",
"appSettings_initialRouteWeightSubtitle": "Poids de départ pour les nouveaux chemins découverts",
"appSettings_routeWeightSuccessIncrement": "Augmentation du poids de réussite",
"appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un itinéraire après une livraison réussie.",
"appSettings_routeWeightFailureDecrement": "Réduction du poids de pénalité",
"appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.",
"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_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.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Veuillez patienter un instant avant de réessayer.",
"appSettings_jumpToOldestUnread": "Accéder au message le plus ancien non lu",
"appSettings_languageHu": "Hongrois",
"appSettings_jumpToOldestUnreadSubtitle": "Lorsque vous ouvrez une conversation contenant des messages non lus, faites défiler la page jusqu'au premier message non lu, plutôt que jusqu'au dernier.",
"appSettings_languageJa": "Japonais",
"appSettings_languageKo": "Coréen",
"radioStats_tooltip": "Statistiques des radios et des réseaux sans fil",
"radioStats_screenTitle": "Statistiques de radio",
"radioStats_notConnected": "Connectez-vous à un appareil pour visualiser les statistiques de la radio.",
"radioStats_firmwareTooOld": "Les statistiques radio nécessitent un firmware compatible v8 ou une version ultérieure.",
"radioStats_waiting": "En attente des données…",
"radioStats_noiseFloor": "Niveau de bruit : {noiseDbm} dBm",
"radioStats_lastRssi": "Dernier RSSI : {rssiDbm} dBm",
"radioStats_lastSnr": "Dernier SNR : {snr} dB",
"radioStats_txAir": "Temps d'antenne à la télévision du Texas (total) : {seconds} s",
"radioStats_rxAir": "Temps d'utilisation de l'appareil RX (total) : {seconds} s",
"radioStats_chartCaption": "Niveau de bruit (dBm) sur les échantillons récents.",
"radioStats_stripNoise": "Niveau de bruit : {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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_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}"
}
+2191
View File
File diff suppressed because it is too large Load Diff
+351 -1
View File
@@ -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",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Nuovo Gruppo",
"contacts_groupName": "Nome gruppo",
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
"contacts_groupNameReserved": "Questo nome del gruppo è riservato",
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1608,8 @@
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
"scanner_chromeRequired": "Browser Chrome richiesto",
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
"scanner_enableBluetooth": "Abilita il Bluetooth",
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
"snrIndicator_lastSeen": "Ultimo accesso",
@@ -1799,5 +1804,350 @@
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
"contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
"contactsSettings_title": "Impostazioni dei contatti",
"settings_contactSettings": "Impostazioni di contatto",
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
"contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.",
"contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente",
"contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.",
"contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori",
"settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti",
"contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente",
"contactsSettings_autoAddTitle": "Scoperta automatica",
"contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti",
"discoveredContacts_noMatching": "Nessun contatto corrispondente",
"contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.",
"discoveredContacts_searchHint": "Cerca contatti scoperti",
"contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze",
"discoveredContacts_addContact": "Aggiungi contatto",
"contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio",
"discoveredContacts_Title": "Contatti scoperti",
"discoveredContacts_contactAdded": "Contatto aggiunto",
"discoveredContacts_deleteContact": "Elimina Contatto",
"discoveredContacts_copyContact": "Copia contatto negli appunti",
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
"common_deleteAll": "Elimina tutto",
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
"map_guessedLocation": "Località indovinata",
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Connessione tramite USB",
"usbScreenStatus": "Seleziona un dispositivo USB",
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.",
"usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.",
"usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.",
"usbErrorInvalidPort": "Seleziona un dispositivo USB valido.",
"usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.",
"usbErrorNotConnected": "Non è collegato alcun dispositivo USB.",
"usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.",
"usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.",
"usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.",
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
"usbErrorPortClosed": "La connessione USB non è attiva.",
"usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Ricerca di dispositivi USB...",
"usbConnectionFailed": "Errore nella connessione USB: {error}",
"usbStatus_notConnected": "Seleziona un dispositivo USB",
"usbStatus_connecting": "Connessione al dispositivo USB...",
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Indirizzo IP",
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Stabilire una connessione tramite TCP",
"tcpPortLabel": "Porta",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Inserisci l'endpoint e connettiti.",
"tcpStatus_connectingTo": "Connessione a {endpoint}...",
"tcpErrorHostRequired": "È necessario fornire un indirizzo IP.",
"tcpErrorPortInvalid": "La dimensione della porta deve essere compresa tra 1 e 65535.",
"tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.",
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
"map_showDiscoveryContacts": "Mostra Contatti di Discovery",
"map_setAsMyLocation": "Imposta come la mia posizione",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.",
"settings_allowByContact": "Consenti in base ai flag di contatto",
"settings_telemetryLocationMode": "Modalità di posizionamento telemetrico",
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
"settings_advertLocation": "Posizione dell'annuncio",
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
"settings_privacy": "Impostazioni sulla privacy",
"settings_denyAll": "Negare tutto",
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
"settings_allowAll": "Consenti tutto",
"contact_info": "Informazioni di Contatto",
"settings_telemetryBaseMode": "Modalità di base di telemetria",
"contact_teleBase": "Base di telemetria",
"contact_teleLoc": "Posizione telemetria",
"contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione",
"contact_clearChat": "Cancella chat",
"contact_telemetry": "Telemetria",
"contact_settings": "Impostazioni di contatto",
"contact_lastSeen": "Ultimo accesso",
"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",
"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.",
"appSettings_maxRouteWeight": "Massimo peso consentito per il percorso",
"appSettings_routeWeightSuccessIncrement": "Aumento del peso del successo",
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso aggiunto a un percorso dopo una consegna riuscita.",
"appSettings_routeWeightFailureDecrement": "Riduzione del peso associato al fallimento",
"appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.",
"appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio",
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnreadSubtitle": "Quando si apre una chat con messaggi non letti, scorrete verso l'alto fino al primo messaggio non letto, invece che al più recente.",
"chat_sendCooldown": "Si prega di attendere un momento prima di inviare nuovamente.",
"appSettings_jumpToOldestUnread": "Vai al messaggio più vecchio non letto",
"appSettings_languageHu": "Ungherese",
"appSettings_languageJa": "Giapponese",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Statistiche per radio e reti",
"radioStats_screenTitle": "Statistiche radio",
"radioStats_notConnected": "Connettiti a un dispositivo per visualizzare le statistiche radio.",
"radioStats_firmwareTooOld": "Le statistiche radio richiedono il firmware versione 8 o successiva.",
"radioStats_noiseFloor": "Livello di rumore: {noiseDbm} dBm",
"radioStats_waiting": "In attesa dei dati…",
"radioStats_lastRssi": "Ultimo valore RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Ultimo SNR: {snr} dB",
"radioStats_txAir": "Tempo di trasmissione in diretta (totale): {seconds} s",
"radioStats_rxAir": "Tempo di trasmissione RX (totale): {seconds} s",
"radioStats_chartCaption": "Livello di rumore (dBm) misurato su campioni recenti.",
"radioStats_stripNoise": "Livello di rumore: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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_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})."
}
+2191
View File
File diff suppressed because it is too large Load Diff
+2191
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+708 -8
View File
@@ -38,9 +38,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_delete => 'Изтрий';
@override
String get common_deleteAll => 'Изтрий всичко';
@override
String get common_close => 'Затвори';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Редактирай';
@@ -108,6 +114,134 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Свържете се чрез TCP';
@override
String get tcpHostLabel => 'IP адрес';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Пристанище';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Въведете крайната точка и свържете се.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Свързване към $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необходим е IP адрес.';
@override
String get tcpErrorPortInvalid => 'Портът трябва да бъде между 1 и 65535.';
@override
String get tcpErrorUnsupported =>
'Транспортът чрез TCP не се поддържа на тази платформа.';
@override
String get tcpErrorTimedOut => 'Връзката TCP изтекла.';
@override
String tcpConnectionFailed(String error) {
return 'Неуспешно е установено TCP връзката: $error';
}
@override
String get usbScreenTitle => 'Свържете се чрез USB';
@override
String get usbScreenSubtitle =>
'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.';
@override
String get usbScreenStatus => 'Изберете USB устройство';
@override
String get usbScreenNote =>
'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.';
@override
String get usbScreenEmptyState =>
'Няма открити USB устройства. Включете едно и опитайте отново.';
@override
String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.';
@override
String get usbErrorDeviceMissing =>
'Избраното USB устройство вече не е налично.';
@override
String get usbErrorInvalidPort => 'Изберете валитно USB устройство.';
@override
String get usbErrorBusy =>
'Друг мол за свързване през USB вече е в процес на изпълнение.';
@override
String get usbErrorNotConnected => 'Няма свързано USB устройство.';
@override
String get usbErrorOpenFailed =>
'Не успях да отворя избраното USB устройство.';
@override
String get usbErrorConnectFailed =>
'Не успях да се свържа с избраното USB устройство.';
@override
String get usbErrorUnsupported =>
'USB серийната комуникация не се поддържа на тази платформа.';
@override
String get usbErrorAlreadyActive => 'USB връзката вече е активирана.';
@override
String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.';
@override
String get usbErrorPortClosed => 'USB връзката не е активна.';
@override
String get usbErrorConnectTimedOut =>
'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.';
@override
String get usbFallbackDeviceName =>
'Устройство за четене на уеб серийни данни';
@override
String get usbStatus_notConnected => 'Изберете USB устройство';
@override
String get usbStatus_connecting => 'Свързване към USB устройство...';
@override
String get usbStatus_searching => 'Търсене на USB устройства...';
@override
String usbConnectionFailed(String error) {
return 'Неуспешно свързване през USB: $error';
}
@override
String get scanner_scanning => 'Сканиране за устройства...';
@@ -150,6 +284,13 @@ class AppLocalizationsBg extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
@override
String get scanner_chromeRequired => 'Изисква се браузър Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.';
@override
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
@@ -234,6 +375,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_longitude => 'Дължина';
@override
String get settings_contactSettings => 'Настройки за контакти';
@override
String get settings_contactSettingsSubtitle =>
'Настройки за добавяне на контакти.';
@override
String get settings_privacyMode => 'Режим на поверителност';
@@ -253,9 +401,60 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Режим на поверителност е деактивиран';
@override
String get settings_privacy => 'Настройки на поверителността';
@override
String get settings_privacySubtitle =>
'Контролирайте каква информация се споделя.';
@override
String get settings_privacySettingsDescription =>
'Изберете каква информация устройството ви споделя с другите.';
@override
String get settings_denyAll => 'Откажи всичко';
@override
String get settings_allowByContact => 'Позволи по флагове за контакт';
@override
String get settings_allowAll => 'Позволи всичко';
@override
String get settings_telemetryBaseMode => 'Базов режим на телеметрия';
@override
String get settings_telemetryLocationMode =>
'Режим на местоположение на телеметрията';
@override
String get settings_telemetryEnvironmentMode =>
'Режим на средата на телеметрията';
@override
String get settings_advertLocation => 'Място на обявата';
@override
String get settings_advertLocationSubtitle =>
'Включи местоположение в обявата';
@override
String get settings_multiAck => 'Множество потвърждения';
@override
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
@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 => 'Изпрати Реклама';
@@ -297,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 =>
@@ -550,6 +749,51 @@ class AppLocalizationsBg extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Автоматично маршрутизирането е деактивирано';
@override
String get appSettings_maxRouteWeight =>
'Максимално допустимо тегло на маршрута';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Максималното тегло, което един маршрут може да събере от успешни доставки.';
@override
String get appSettings_initialRouteWeight =>
'Първоначална тежест на маршрута';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Начално тегло за новооткрити маршрути';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Увеличение на теглото за успех';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Тегло, добавено към път след успешно доставяне.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Намаляване на теглото, свързано с неуспех';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Тегло, което е било премахнато от пътя след неуспешен опит за доставка.';
@override
String get appSettings_maxMessageRetries =>
'Максимален брой опити за изпращане на съобщение';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батерия';
@@ -757,6 +1001,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Името на групата е задължително.';
@override
String get contacts_groupNameReserved => 'Това име на група е запазено';
@override
String contacts_groupAlreadyExists(String name) {
return 'Групата \"$name\" вече съществува.';
@@ -796,6 +1043,42 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Последно видян $days дни преди.';
}
@override
String get contact_info => 'Контактна информация';
@override
String get contact_settings => 'Настройки за контакти';
@override
String get contact_telemetry => 'Телеметрия';
@override
String get contact_lastSeen => 'Последно видян';
@override
String get contact_clearChat => 'Изчисти чата';
@override
String get contact_teleBase => 'Базата данни за телеметрия';
@override
String get contact_teleBaseSubtitle =>
'Позволи споделяне на ниво на батерията и основна телеметрия';
@override
String get contact_teleLoc => 'Местоположение на телеметрията';
@override
String get contact_teleLocSubtitle =>
'Позволи споделяне на данни за местоположение';
@override
String get contact_teleEnv => 'Среда на телеметрия';
@override
String get contact_teleEnvSubtitle =>
'Позволи споделяне на данни от средносферните датчици';
@override
String get channels_title => 'Канали';
@@ -822,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 => 'Личен';
@@ -964,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 => 'Изпрати съобщение, за да започнеш.';
@@ -983,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 => 'Въведете съобщение...';
@@ -1180,6 +1471,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_successes => 'Успехи';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => 'Премахни пътя';
@@ -1282,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 => 'Отваряне на връзката?';
@@ -1363,9 +1663,30 @@ 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 => 'Споделете маркер тук';
@override
String get map_setAsMyLocation => 'Задайте като моя местоположение';
@override
String get map_pinLabel => 'Етикетиране на пин';
@@ -1411,6 +1732,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_otherNodes => 'Други възли';
@override
String get map_showOverlaps => 'Покриване на ключа на повтаряча';
@override
String get map_keyPrefix => 'Префикс на ключа';
@@ -1426,12 +1750,25 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Покажи споделени маркери';
@override
String get map_showGuessedLocations =>
'Покажете местоположенията на предположените възли.';
@override
String get map_showDiscoveryContacts => 'Покажи контакти за откриване';
@override
String get map_guessedLocation => 'Предполагано местоположение';
@override
String get map_lastSeenTime => 'Последна видяна дата';
@override
String get map_sharedPin => 'Споделено копие';
@override
String get map_sharedAt => 'Споделено';
@override
String get map_joinRoom => 'Присъедини се към стаята';
@@ -1445,6 +1782,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_runTrace => 'Изпълни Път на Следване';
@override
String get map_runTraceWithReturnPath => 'Върни се по същия път.';
@override
String get map_removeLast => 'Премахни Последно';
@@ -1722,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 => 'Статус';
@@ -1759,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 => 'Статус на повтарянето';
@@ -2135,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 => 'Изпраща рекламен пакет';
@@ -2918,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 => 'Честота';
@@ -3112,4 +3506,310 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Последно видян';
@override
String get contactsSettings_title => 'Настройки на контактите';
@override
String get contactsSettings_autoAddTitle => 'Автоматично откриване';
@override
String get contactsSettings_otherTitle =>
'Други настройки свързани с контакти';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматично добавяне на потребители';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Позволи на спътника да добавя автоматично откритите потребители.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматично добавяне на повтарящи се елементи';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматично добавяне на сървъри на стаите';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Позволи на спътника да добавя автоматично откритите сървъри на стаите.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматично добавяне на датчици';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Позволи на спътника да добавя автоматично откритите датчици.';
@override
String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.';
@override
String get discoveredContacts_Title => 'Открити контакти';
@override
String get discoveredContacts_noMatching => 'Няма съвпадащи контакти';
@override
String get discoveredContacts_searchHint => 'Търсене на открити контакти';
@override
String get discoveredContacts_contactAdded => 'Контакт добавен';
@override
String get discoveredContacts_addContact => 'Добави контакт';
@override
String get discoveredContacts_copyContact => 'Копирай контакт в клипборда';
@override
String get discoveredContacts_deleteContact => 'Изтрий контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Изтриване на Всички Открити Контакти';
@override
String get discoveredContacts_deleteContactAllContent =>
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
@override
String get chat_sendCooldown =>
'Моля, изчакайте малко, преди да изпратите отново.';
@override
String get appSettings_jumpToOldestUnread =>
'Преминете към най-старата непочетена статия';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.';
@override
String get appSettings_languageHu => 'Унгарски';
@override
String get appSettings_languageJa => 'Японски';
@override
String get appSettings_languageKo => 'Корейски';
@override
String get radioStats_tooltip => 'Статистика за радио и мрежа';
@override
String get radioStats_screenTitle =>
'Статистически данни за радиопредаванията';
@override
String get radioStats_notConnected =>
'Свържете се с устройство, за да видите статистически данни за радиопредаване.';
@override
String get radioStats_firmwareTooOld =>
'Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.';
@override
String get radioStats_waiting => 'Изчакване на данни…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Ниво на шума: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Последен RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Последна стойност на SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Време на въздух (общо): $seconds секунди';
}
@override
String radioStats_rxAir(int seconds) {
return 'Общо време на използване на RX (в секунди): $seconds с';
}
@override
String get radioStats_chartCaption =>
'Ниво на шума (dBm) за последните измервания.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Ниво на шума: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Извличане на данни за радиото…';
@override
String get radioStats_settingsTile => 'Статистически данни за радиостанции';
@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';
@override
String get scanner_linuxPairingHidePin => 'Скриване на PIN кода';
@override
String get scanner_linuxPairingPinTitle => 'PIN за съвпадение чрез Bluetooth';
@override
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 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';
}
+716 -14
View File
@@ -38,9 +38,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get common_delete => 'Löschen';
@override
String get common_deleteAll => 'Alles löschen';
@override
String get common_close => 'Schließen';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Bearbeiten';
@@ -108,6 +114,137 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Verbinden über TCP';
@override
String get tcpHostLabel => 'IP-Adresse';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected =>
'Geben Sie den Endpunkt ein und verbinden Sie sich.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Verbindung zu $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Eine IP-Adresse ist erforderlich.';
@override
String get tcpErrorPortInvalid =>
'Die Portnummer muss zwischen 1 und 65535 liegen.';
@override
String get tcpErrorUnsupported =>
'Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.';
@override
String get tcpErrorTimedOut => 'Die TCP-Verbindung ist abgelaufen.';
@override
String tcpConnectionFailed(String error) {
return 'Fehler beim TCP-Verbindungsaufbau: $error';
}
@override
String get usbScreenTitle => 'Verbinden über USB';
@override
String get usbScreenSubtitle =>
'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.';
@override
String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus';
@override
String get usbScreenNote =>
'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.';
@override
String get usbScreenEmptyState =>
'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
@override
String get usbErrorPermissionDenied =>
'Die USB-Berechtigung wurde abgelehnt.';
@override
String get usbErrorDeviceMissing =>
'Das ausgewählte USB-Gerät ist nicht mehr verfügbar.';
@override
String get usbErrorInvalidPort => 'Wählen Sie ein gültiges USB-Gerät aus.';
@override
String get usbErrorBusy =>
'Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.';
@override
String get usbErrorNotConnected => 'Es ist kein USB-Gerät angeschlossen.';
@override
String get usbErrorOpenFailed =>
'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.';
@override
String get usbErrorConnectFailed =>
'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.';
@override
String get usbErrorUnsupported =>
'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.';
@override
String get usbErrorAlreadyActive =>
'Eine USB-Verbindung ist bereits hergestellt.';
@override
String get usbErrorNoDeviceSelected => 'Kein USB-Gerät wurde ausgewählt.';
@override
String get usbErrorPortClosed => 'Die USB-Verbindung ist nicht aktiv.';
@override
String get usbErrorConnectTimedOut =>
'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.';
@override
String get usbFallbackDeviceName => 'Web-Serielle Geräte';
@override
String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus';
@override
String get usbStatus_connecting => 'Verbindung zum USB-Gerät...';
@override
String get usbStatus_searching => 'Suche nach USB-Geräten...';
@override
String usbConnectionFailed(String error) {
return 'Fehler beim USB-Verbindungsaufbau: $error';
}
@override
String get scanner_scanning => 'Scannen nach Geräten...';
@@ -150,6 +287,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
@override
String get scanner_chromeRequired => 'Chrome Browser erforderlich';
@override
String get scanner_chromeRequiredMessage =>
'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.';
@override
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
@@ -233,6 +377,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_longitude => 'Längengrad';
@override
String get settings_contactSettings => 'Kontakteinstellungen';
@override
String get settings_contactSettingsSubtitle =>
'Einstellungen für das Hinzufügen von Kontakten';
@override
String get settings_privacyMode => 'Privatsphäreeinstellung';
@@ -250,9 +401,58 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
@override
String get settings_privacy => 'Datenschutzeinstellungen';
@override
String get settings_privacySubtitle =>
'Steuern Sie die Informationen, die freigegeben werden.';
@override
String get settings_privacySettingsDescription =>
'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.';
@override
String get settings_denyAll => 'Alle ablehnen';
@override
String get settings_allowByContact => 'Zulassen durch Kontaktflaggen';
@override
String get settings_allowAll => 'Alles zulassen';
@override
String get settings_telemetryBaseMode => 'Telemetrie-Basismodus';
@override
String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus';
@override
String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus';
@override
String get settings_advertLocation => 'Anzeigenort';
@override
String get settings_advertLocationSubtitle =>
'Ort in der Anzeige einbeziehen';
@override
String get settings_multiAck => 'Mehrere Bestätigungen';
@override
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
@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';
@@ -293,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';
@@ -547,6 +747,49 @@ class AppLocalizationsDe extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatische Routenrotation deaktiviert';
@override
String get appSettings_maxRouteWeight => 'Maximale Gesamtstreckenlänge';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.';
@override
String get appSettings_initialRouteWeight => 'Anfangs-Streckengewicht';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Ausgangsgewicht für neu entdeckte Pfade';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Erhöhung des Erfolgsgewichts';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Reduzierung des Gewichts bei Fehlern';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde';
@override
String get appSettings_maxMessageRetries =>
'Maximale Anzahl an Wiederholungsversuchen';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Akku';
@@ -754,6 +997,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
@override
String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert';
@override
String contacts_groupAlreadyExists(String name) {
return 'Die Gruppe \"$name\" existiert bereits.';
@@ -770,29 +1016,64 @@ class AppLocalizationsDe extends AppLocalizations {
String get contacts_noMembers => 'Keine Mitglieder';
@override
String get contacts_lastSeenNow => 'gerade gesehen';
String get contacts_lastSeenNow => 'kürzlich';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Letzte Sichtung vor $minutes Minuten.';
return '~ $minutes Min.';
}
@override
String get contacts_lastSeenHourAgo => 'Letzte Sichtung vor 1 Stunde.';
String get contacts_lastSeenHourAgo => '~ 1 Std.';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Letzte Sichtung vor $hours Stunden.';
return '~ $hours Std.';
}
@override
String get contacts_lastSeenDayAgo => 'Letzte Sichtung vor 1 Tag';
String get contacts_lastSeenDayAgo => '~ 1 Tag';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Letzte Sichtung $days Tage zuvor';
return '~ $days Tage';
}
@override
String get contact_info => 'Kontaktinformationen';
@override
String get contact_settings => 'Kontakteinstellungen';
@override
String get contact_telemetry => 'Telemetrie';
@override
String get contact_lastSeen => 'Zuletzt gesehen';
@override
String get contact_clearChat => 'Chat löschen';
@override
String get contact_teleBase => 'Telemetriebasis';
@override
String get contact_teleBaseSubtitle =>
'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie';
@override
String get contact_teleLoc => 'Telemetrieort';
@override
String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen';
@override
String get contact_teleEnv => 'Telemetrieumgebung';
@override
String get contact_teleEnvSubtitle =>
'Teilen von Umgebungsensordaten zulassen';
@override
String get channels_title => 'Kanäle';
@@ -819,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';
@@ -965,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.';
@@ -984,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...';
@@ -1181,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';
@@ -1281,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?';
@@ -1362,9 +1660,30 @@ 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.';
@override
String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen';
@override
String get map_pinLabel => 'Pin Name';
@@ -1410,6 +1729,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Knoten';
@override
String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
@override
String get map_keyPrefix => 'Schlüsselpräfix';
@@ -1425,12 +1747,25 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker';
@override
String get map_showGuessedLocations =>
'Zeige die vermuteten Knotenpositionen';
@override
String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen';
@override
String get map_guessedLocation => 'Geschätzter Ort';
@override
String get map_lastSeenTime => 'Letzte Sichtung';
@override
String get map_sharedPin => 'Gemeinsames Passwort';
@override
String get map_sharedAt => 'Geteilt';
@override
String get map_joinRoom => 'Beitreten Sie dem Raum';
@@ -1444,6 +1779,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_runTrace => 'Pfadverlauf ausführen';
@override
String get map_runTraceWithReturnPath =>
'Auf dem gleichen Pfad zurückkehren.';
@override
String get map_removeLast => 'Letztes Entfernen';
@@ -1721,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';
@@ -1756,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';
@@ -2136,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';
@@ -2924,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';
@@ -3121,4 +3514,313 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
@override
String get contactsSettings_title => 'Kontakteinstellungen';
@override
String get contactsSettings_autoAddTitle => 'Automatische Erkennung';
@override
String get contactsSettings_otherTitle =>
'Weitere Einstellungen zu Kontakten';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automatische Hinzufügung von Benutzern';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automatisch Repeater hinzufügen';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatisch Raumservers hinzufügen';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automatisch Sensoren hinzufügen';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen';
@override
String get contactsSettings_overwriteOldestTitle =>
'Überschreiben des Ältesten';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.';
@override
String get discoveredContacts_Title => 'Entdeckte Kontakte';
@override
String get discoveredContacts_noMatching => 'Keine passenden Kontakte';
@override
String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen';
@override
String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt';
@override
String get discoveredContacts_addContact => 'Kontakt hinzufügen';
@override
String get discoveredContacts_copyContact =>
'Kontakt in die Zwischenablage kopieren';
@override
String get discoveredContacts_deleteContact => 'Kontakt löschen';
@override
String get discoveredContacts_deleteContactAll =>
'Alle entdeckten Kontakte löschen';
@override
String get discoveredContacts_deleteContactAllContent =>
'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?';
@override
String get chat_sendCooldown =>
'Bitte warten Sie einen Moment, bevor Sie erneut senden.';
@override
String get appSettings_jumpToOldestUnread =>
'Zum ältesten, nicht gelesenen Eintrag springen';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.';
@override
String get appSettings_languageHu => 'Ungarisch';
@override
String get appSettings_languageJa => 'Japanisch';
@override
String get appSettings_languageKo => 'Koreanisch';
@override
String get radioStats_tooltip => 'Daten zu Radio- und Mesh-Netzwerken';
@override
String get radioStats_screenTitle => 'Senderinformationen';
@override
String get radioStats_notConnected =>
'Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.';
@override
String get radioStats_firmwareTooOld =>
'Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.';
@override
String get radioStats_waiting => 'Warte auf Daten…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Rauschpegel: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Letzter RSSI-Wert: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Letzter SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Gesamt-TX-Zeit: $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Gesamt-RX-Zeit: $seconds s';
}
@override
String get radioStats_chartCaption =>
'Rauschpegel (dBm) basierend auf den letzten Messwerten.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Rauschpegel: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Abrufen von Radiostatus…';
@override
String get radioStats_settingsTile => 'Senderinformationen';
@override
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';
@override
String get scanner_linuxPairingHidePin => 'PIN ausblenden';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth-Paarungs-PIN';
@override
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';
}
+695 -17
View File
@@ -38,9 +38,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_delete => 'Delete';
@override
String get common_deleteAll => 'Delete All';
@override
String get common_close => 'Close';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Edit';
@@ -108,6 +114,132 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Connect over TCP';
@override
String get tcpHostLabel => 'IP Address';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Enter endpoint and connect';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connecting to $endpoint...';
}
@override
String get tcpErrorHostRequired => 'IP address is required.';
@override
String get tcpErrorPortInvalid => 'Port must be between 1 and 65535.';
@override
String get tcpErrorUnsupported =>
'TCP transport is not supported on this platform.';
@override
String get tcpErrorTimedOut => 'TCP connection timed out.';
@override
String tcpConnectionFailed(String error) {
return 'TCP connection failed: $error';
}
@override
String get usbScreenTitle => 'Connect over USB';
@override
String get usbScreenSubtitle =>
'Choose a detected serial device and connect directly to your MeshCore node.';
@override
String get usbScreenStatus => 'Select a USB device';
@override
String get usbScreenNote =>
'USB serial is active on supported Android devices and desktop platforms.';
@override
String get usbScreenEmptyState =>
'No USB devices found. Plug one in and refresh.';
@override
String get usbErrorPermissionDenied => 'USB permission was denied.';
@override
String get usbErrorDeviceMissing =>
'The selected USB device is no longer available.';
@override
String get usbErrorInvalidPort => 'Select a valid USB device.';
@override
String get usbErrorBusy =>
'Another USB connection request is already in progress.';
@override
String get usbErrorNotConnected => 'No USB device is connected.';
@override
String get usbErrorOpenFailed => 'Failed to open the selected USB device.';
@override
String get usbErrorConnectFailed =>
'Failed to connect to the selected USB device.';
@override
String get usbErrorUnsupported =>
'USB serial is not supported on this platform.';
@override
String get usbErrorAlreadyActive => 'A USB connection is already active.';
@override
String get usbErrorNoDeviceSelected => 'No USB device was selected.';
@override
String get usbErrorPortClosed => 'The USB connection is not open.';
@override
String get usbErrorConnectTimedOut =>
'Connection timed out. Make sure the device has USB Companion firmware.';
@override
String get usbFallbackDeviceName => 'Web Serial Device';
@override
String get usbStatus_notConnected => 'Select a USB device';
@override
String get usbStatus_connecting => 'Connecting to USB device...';
@override
String get usbStatus_searching => 'Searching for USB devices...';
@override
String usbConnectionFailed(String error) {
return 'USB connection failed: $error';
}
@override
String get scanner_scanning => 'Scanning for devices...';
@@ -149,6 +281,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Please turn on Bluetooth to scan for devices';
@override
String get scanner_chromeRequired => 'Chrome Browser Required';
@override
String get scanner_chromeRequiredMessage =>
'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.';
@override
String get scanner_enableBluetooth => 'Enable Bluetooth';
@@ -232,6 +371,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Contact Settings';
@override
String get settings_contactSettingsSubtitle =>
'Settings for how contacts are added.';
@override
String get settings_privacyMode => 'Privacy Mode';
@@ -249,9 +395,56 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privacy mode disabled';
@override
String get settings_privacy => 'Privacy Settings';
@override
String get settings_privacySubtitle => 'Control what information is shared.';
@override
String get settings_privacySettingsDescription =>
'Choose what information your device shares with others.';
@override
String get settings_denyAll => 'Deny all';
@override
String get settings_allowByContact => 'Allow by contact flags';
@override
String get settings_allowAll => 'Allow all';
@override
String get settings_telemetryBaseMode => 'Telemetry Base Mode';
@override
String get settings_telemetryLocationMode => 'Telemetry Location Mode';
@override
String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode';
@override
String get settings_advertLocation => 'Advert Location';
@override
String get settings_advertLocationSubtitle => 'Include location in advert.';
@override
String get settings_multiAck => 'Multi-ACKs';
@override
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
@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';
@@ -291,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';
@@ -541,6 +734,48 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Auto route rotation disabled';
@override
String get appSettings_maxRouteWeight => 'Max Route Weight';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximum weight a path can accumulate from successful deliveries';
@override
String get appSettings_initialRouteWeight => 'Initial Route Weight';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Starting weight for newly discovered paths';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Success Weight Increment';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Weight added to a path after successful delivery';
@override
String get appSettings_routeWeightFailureDecrement =>
'Failure Weight Decrement';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Weight removed from a path after failed delivery';
@override
String get appSettings_maxMessageRetries => 'Max Message Retries';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Number of retry attempts before marking a message as failed';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Battery';
@@ -746,6 +981,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Group name is required';
@override
String get contacts_groupNameReserved => 'This group name is reserved';
@override
String contacts_groupAlreadyExists(String name) {
return 'Group \"$name\" already exists';
@@ -761,29 +999,63 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_noMembers => 'No members';
@override
String get contacts_lastSeenNow => 'Last seen now';
String get contacts_lastSeenNow => 'recently';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Last seen $minutes mins ago';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago';
String get contacts_lastSeenHourAgo => '~ 1 hour';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Last seen $hours hours ago';
return '~ $hours hours';
}
@override
String get contacts_lastSeenDayAgo => 'Last seen 1 day ago';
String get contacts_lastSeenDayAgo => '~ 1 day';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Last seen $days days ago';
return '~ $days days';
}
@override
String get contact_info => 'Contact Info';
@override
String get contact_settings => 'Contact Settings';
@override
String get contact_telemetry => 'Telemetry';
@override
String get contact_lastSeen => 'Last seen';
@override
String get contact_clearChat => 'Clear Chat';
@override
String get contact_teleBase => 'Telemetry Base';
@override
String get contact_teleBaseSubtitle =>
'Allow sharing battery level and basic telemetry';
@override
String get contact_teleLoc => 'Telemetry Location';
@override
String get contact_teleLocSubtitle => 'Allow sharing location data';
@override
String get contact_teleEnv => 'Telemetry Environment';
@override
String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data';
@override
String get channels_title => 'Channels';
@@ -810,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';
@@ -949,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';
@@ -968,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...';
@@ -1163,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';
@@ -1260,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?';
@@ -1341,9 +1630,30 @@ 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';
@override
String get map_setAsMyLocation => 'Set as my location';
@override
String get map_pinLabel => 'Pin label';
@@ -1389,6 +1699,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_otherNodes => 'Other Nodes';
@override
String get map_showOverlaps => 'Repeater Key Overlaps';
@override
String get map_keyPrefix => 'Key Prefix';
@@ -1404,12 +1717,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Show shared markers';
@override
String get map_showGuessedLocations => 'Show guessed node locations';
@override
String get map_showDiscoveryContacts => 'Show Discovery Contacts';
@override
String get map_guessedLocation => 'Guessed location';
@override
String get map_lastSeenTime => 'Last Seen Time';
@override
String get map_sharedPin => 'Shared pin';
@override
String get map_sharedAt => 'Shared';
@override
String get map_joinRoom => 'Join Room';
@@ -1420,7 +1745,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
@override
String get map_runTrace => 'Run Path Trace';
String get map_runTrace => 'Run path trace';
@override
String get map_runTraceWithReturnPath => 'Return back on the same path.';
@override
String get map_removeLast => 'Remove Last';
@@ -1586,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';
@@ -1694,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';
@@ -1729,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';
@@ -2097,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';
@@ -2875,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';
@@ -3065,4 +3446,301 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Last seen';
@override
String get contactsSettings_title => 'Contacts settings';
@override
String get contactsSettings_autoAddTitle => 'Automatic Discovery';
@override
String get contactsSettings_otherTitle => 'Other contact related settings';
@override
String get contactsSettings_autoAddUsersTitle => 'Auto-add users';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Allow the companion to automatically add discovered users.';
@override
String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Allow the companion to automatically add discovered repeaters.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Auto-add room servers';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Allow the companion to automatically add discovered room servers.';
@override
String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Allow the companion to automatically add discovered sensors.';
@override
String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'When the contact list is full, the oldest non-favorited contact will be replaced.';
@override
String get discoveredContacts_Title => 'Discovered Contacts';
@override
String get discoveredContacts_noMatching => 'No matching contacts';
@override
String get discoveredContacts_searchHint => 'Search discovered contacts';
@override
String get discoveredContacts_contactAdded => 'Contact added';
@override
String get discoveredContacts_addContact => 'Add Contact';
@override
String get discoveredContacts_copyContact => 'Copy Contact to clipboard';
@override
String get discoveredContacts_deleteContact => 'Delete Discovered Contact';
@override
String get discoveredContacts_deleteContactAll =>
'Delete All Discovered Contacts';
@override
String get discoveredContacts_deleteContactAllContent =>
'Are you sure you want to delete all discovered contacts?';
@override
String get chat_sendCooldown => 'Please wait a moment before sending again.';
@override
String get appSettings_jumpToOldestUnread => 'Jump to oldest unread';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'When opening a chat with unread messages, scroll to the first unread instead of the latest.';
@override
String get appSettings_languageHu => 'Hungarian';
@override
String get appSettings_languageJa => 'Japanese';
@override
String get appSettings_languageKo => 'Korean';
@override
String get radioStats_tooltip => 'Radio & mesh stats';
@override
String get radioStats_screenTitle => 'Radio stats';
@override
String get radioStats_notConnected =>
'Connect to a device to view radio statistics.';
@override
String get radioStats_firmwareTooOld =>
'Radio statistics require companion firmware v8 or newer.';
@override
String get radioStats_waiting => 'Waiting for data…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Noise floor: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Last RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Last SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX airtime (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX airtime (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Noise floor (dBm) over recent samples.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Noise floor: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Fetching radio stats…';
@override
String get radioStats_settingsTile => 'Radio stats';
@override
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';
@override
String get scanner_linuxPairingHidePin => 'Hide PIN';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth Pairing PIN';
@override
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';
}
+716 -13
View File
@@ -38,9 +38,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_delete => 'Eliminar';
@override
String get common_deleteAll => 'Eliminar todo';
@override
String get common_close => 'Cerrar';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Editar';
@@ -108,6 +114,135 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Establecer conexión a través de TCP';
@override
String get tcpHostLabel => 'Dirección IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Puerto';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Ingrese la dirección final y conecte.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Conectándose a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Se requiere la dirección IP.';
@override
String get tcpErrorPortInvalid => 'El puerto debe estar entre 1 y 65535.';
@override
String get tcpErrorUnsupported =>
'El protocolo de transporte TCP no está soportado en esta plataforma.';
@override
String get tcpErrorTimedOut => 'La conexión TCP ha caducado.';
@override
String tcpConnectionFailed(String error) {
return 'Error en la conexión TCP: $error';
}
@override
String get usbScreenTitle => 'Conecte mediante USB';
@override
String get usbScreenSubtitle =>
'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.';
@override
String get usbScreenStatus => 'Seleccione un dispositivo USB';
@override
String get usbScreenNote =>
'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.';
@override
String get usbScreenEmptyState =>
'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.';
@override
String get usbErrorPermissionDenied =>
'Se denegó el permiso de acceso a través de USB.';
@override
String get usbErrorDeviceMissing =>
'El dispositivo USB seleccionado ya no está disponible.';
@override
String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.';
@override
String get usbErrorBusy =>
'Ya se ha iniciado una solicitud de conexión USB adicional.';
@override
String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.';
@override
String get usbErrorOpenFailed =>
'No se pudo abrir el dispositivo USB seleccionado.';
@override
String get usbErrorConnectFailed =>
'No se pudo conectar con el dispositivo USB seleccionado.';
@override
String get usbErrorUnsupported =>
'La comunicación serial a través de USB no está soportada en esta plataforma.';
@override
String get usbErrorAlreadyActive => 'La conexión USB ya está activa.';
@override
String get usbErrorNoDeviceSelected =>
'No se ha seleccionado ningún dispositivo USB.';
@override
String get usbErrorPortClosed => 'La conexión USB no está activa.';
@override
String get usbErrorConnectTimedOut =>
'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Dispositivo de serie web';
@override
String get usbStatus_notConnected => 'Seleccione un dispositivo USB';
@override
String get usbStatus_connecting => 'Conectándose al dispositivo USB...';
@override
String get usbStatus_searching => 'Buscando dispositivos USB...';
@override
String usbConnectionFailed(String error) {
return 'Error al conectar mediante USB: $error';
}
@override
String get scanner_scanning => 'Escaneando dispositivos...';
@@ -150,6 +285,13 @@ class AppLocalizationsEs extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, active el Bluetooth para escanear dispositivos.';
@override
String get scanner_chromeRequired => 'Navegador Chrome requerido';
@override
String get scanner_chromeRequiredMessage =>
'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.';
@override
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
@@ -233,6 +375,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_longitude => 'Longitud';
@override
String get settings_contactSettings => 'Configuración de contacto';
@override
String get settings_contactSettingsSubtitle =>
'Configuración de cómo se agregan los contactos.';
@override
String get settings_privacyMode => 'Modo Privacidad';
@@ -250,9 +399,59 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
@override
String get settings_privacy => 'Configuración de privacidad';
@override
String get settings_privacySubtitle =>
'Controlar qué información se comparte.';
@override
String get settings_privacySettingsDescription =>
'Elige qué información comparte tu dispositivo con otros.';
@override
String get settings_denyAll => 'Denegar todo';
@override
String get settings_allowByContact => 'Permitir por banderas de contacto';
@override
String get settings_allowAll => 'Permitir todo';
@override
String get settings_telemetryBaseMode => 'Modo base de telemetría';
@override
String get settings_telemetryLocationMode =>
'Modo de ubicación de telemetría';
@override
String get settings_telemetryEnvironmentMode =>
'Modo de entorno de telemetría';
@override
String get settings_advertLocation => 'Ubicación de anuncio';
@override
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
@override
String get settings_multiAck => 'Múltiples respuestas de confirmación';
@override
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
@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';
@@ -295,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';
@@ -548,6 +747,49 @@ class AppLocalizationsEs extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotación de ruta automática desactivada';
@override
String get appSettings_maxRouteWeight => 'Peso máximo permitido para la ruta';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Peso máximo que una ruta puede acumular gracias a entregas exitosas.';
@override
String get appSettings_initialRouteWeight => 'Peso inicial de la ruta';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Peso inicial para rutas recién descubiertas';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Incremento de peso para el éxito';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Peso añadido a una ruta después de una entrega exitosa.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Reducción del peso asociado al fallo';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Peso retirado de un camino después de un intento de entrega fallido.';
@override
String get appSettings_maxMessageRetries =>
'Número máximo de reintentos de envío de mensajes';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Número de intentos de reintento antes de marcar un mensaje como fallido.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batería';
@@ -755,6 +997,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
@override
String get contacts_groupNameReserved =>
'Este nombre de grupo está reservado';
@override
String contacts_groupAlreadyExists(String name) {
return 'El grupo \"$name\" ya existe';
@@ -775,25 +1021,61 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Última vez visto hace $minutes minutos.';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora';
String get contacts_lastSeenHourAgo => '~ 1 hora';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Última vez visto hace $hours horas.';
return '~ $hours horas';
}
@override
String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día';
String get contacts_lastSeenDayAgo => '~ 1 día';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Última vez visto hace $days días.';
return '~ $days días';
}
@override
String get contact_info => 'Información de contacto';
@override
String get contact_settings => 'Configuración de contacto';
@override
String get contact_telemetry => 'Telemetría';
@override
String get contact_lastSeen => 'Visto por última vez';
@override
String get contact_clearChat => 'Borrar chat';
@override
String get contact_teleBase => 'Base de Telemetría';
@override
String get contact_teleBaseSubtitle =>
'Permitir el intercambio de nivel de batería y telemetría básica';
@override
String get contact_teleLoc => 'Ubicación de telemetría';
@override
String get contact_teleLocSubtitle =>
'Permitir el intercambio de datos de ubicación';
@override
String get contact_teleEnv => 'Entorno de Telemetría';
@override
String get contact_teleEnvSubtitle =>
'Permitir el intercambio de datos de sensores de entorno';
@override
String get channels_title => 'Canales';
@@ -820,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';
@@ -964,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';
@@ -983,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...';
@@ -1178,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';
@@ -1279,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?';
@@ -1360,9 +1659,30 @@ 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í';
@override
String get map_setAsMyLocation => 'Establecer mi ubicación';
@override
String get map_pinLabel => 'Etiqueta de marcador';
@@ -1408,6 +1728,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_otherNodes => 'Otros Nodos';
@override
String get map_showOverlaps => 'Superposiciones de tecla repetidora';
@override
String get map_keyPrefix => 'Prefijo de clave';
@@ -1423,12 +1746,25 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
@override
String get map_showGuessedLocations =>
'Mostrar las ubicaciones estimadas de los nodos.';
@override
String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento';
@override
String get map_guessedLocation => 'Ubicación estimada';
@override
String get map_lastSeenTime => 'Última vez que se vio';
@override
String get map_sharedPin => 'Pin compartido';
@override
String get map_sharedAt => 'Compartido';
@override
String get map_joinRoom => 'Únete a la sala';
@@ -1441,6 +1777,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
@override
String get map_runTraceWithReturnPath => 'Volver atrás por el mismo camino.';
@override
String get map_removeLast => 'Eliminar último';
@@ -1719,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';
@@ -1754,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';
@@ -2130,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';
@@ -2918,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';
@@ -3113,4 +3506,314 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Visto por última vez';
@override
String get contactsSettings_title => 'Configuración de contactos';
@override
String get contactsSettings_autoAddTitle => 'Detección automática';
@override
String get contactsSettings_otherTitle =>
'Otras configuraciones relacionadas con el contacto';
@override
String get contactsSettings_autoAddUsersTitle =>
'Agregar usuarios automáticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Agregar repetidores automáticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Permitir que el compañero agregue automáticamente los repetidores descubiertos.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Agregar automáticamente servidores de sala';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Agregar sensores automáticamente';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Permitir que el compañero agregue automáticamente los sensores descubiertos.';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sobreescribir el más antiguo';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.';
@override
String get discoveredContacts_Title => 'Contactos descubiertos';
@override
String get discoveredContacts_noMatching =>
'No se encontraron contactos coincidentes';
@override
String get discoveredContacts_searchHint => 'Buscar contactos descubiertos';
@override
String get discoveredContacts_contactAdded => 'Contacto agregado';
@override
String get discoveredContacts_addContact => 'Agregar contacto';
@override
String get discoveredContacts_copyContact =>
'Copiar contacto al portapapeles';
@override
String get discoveredContacts_deleteContact => 'Eliminar contacto';
@override
String get discoveredContacts_deleteContactAll =>
'Eliminar Todos los Contactos Descubiertos';
@override
String get discoveredContacts_deleteContactAllContent =>
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
@override
String get chat_sendCooldown =>
'Por favor, espere un momento antes de reenviar.';
@override
String get appSettings_jumpToOldestUnread =>
'Salta a los mensajes más antiguos sin leer';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.';
@override
String get appSettings_languageHu => 'Húngaro';
@override
String get appSettings_languageJa => 'Japonés';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Estadísticas de radio y malla';
@override
String get radioStats_screenTitle => 'Estadísticas de radio';
@override
String get radioStats_notConnected =>
'Conéctese a un dispositivo para visualizar estadísticas de radio.';
@override
String get radioStats_firmwareTooOld =>
'Las estadísticas de radio requieren un firmware compatible v8 o posterior.';
@override
String get radioStats_waiting => 'Esperando datos…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Nivel de ruido: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Último RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Último SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tiempo de emisión en Texas (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tiempo de transmisión de RX (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Nivel de ruido (dBm) en muestras recientes.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Nivel de ruido: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Obteniendo estadísticas de la radio…';
@override
String get radioStats_settingsTile => 'Estadísticas de radio';
@override
String get radioStats_settingsSubtitle =>
'Nivel de ruido, RSSI, SNR y tiempo de transmisión';
@override
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 para emparejar dispositivos Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
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';
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+711 -8
View File
@@ -38,9 +38,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_delete => 'Elimina';
@override
String get common_deleteAll => 'Elimina tutto';
@override
String get common_close => 'Chiudi';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Modifica';
@@ -108,6 +114,137 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Stabilire una connessione tramite TCP';
@override
String get tcpHostLabel => 'Indirizzo IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Porta';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Inserisci l\'endpoint e connettiti.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connessione a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'È necessario fornire un indirizzo IP.';
@override
String get tcpErrorPortInvalid =>
'La dimensione della porta deve essere compresa tra 1 e 65535.';
@override
String get tcpErrorUnsupported =>
'Il protocollo TCP non è supportato su questa piattaforma.';
@override
String get tcpErrorTimedOut => 'La connessione TCP è scaduta.';
@override
String tcpConnectionFailed(String error) {
return 'Impossibile stabilire la connessione TCP: $error';
}
@override
String get usbScreenTitle => 'Connessione tramite USB';
@override
String get usbScreenSubtitle =>
'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.';
@override
String get usbScreenStatus => 'Seleziona un dispositivo USB';
@override
String get usbScreenNote =>
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
@override
String get usbScreenEmptyState =>
'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.';
@override
String get usbErrorPermissionDenied =>
'È stato negato l\'accesso tramite USB.';
@override
String get usbErrorDeviceMissing =>
'Il dispositivo USB selezionato non è più disponibile.';
@override
String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.';
@override
String get usbErrorBusy =>
'Un\'altra richiesta di connessione tramite USB è già in corso.';
@override
String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.';
@override
String get usbErrorOpenFailed =>
'Impossibile aprire il dispositivo USB selezionato.';
@override
String get usbErrorConnectFailed =>
'Impossibile connettersi al dispositivo USB selezionato.';
@override
String get usbErrorUnsupported =>
'La comunicazione seriale tramite USB non è supportata su questa piattaforma.';
@override
String get usbErrorAlreadyActive => 'La connessione USB è già attiva.';
@override
String get usbErrorNoDeviceSelected =>
'Non è stato selezionato alcun dispositivo USB.';
@override
String get usbErrorPortClosed => 'La connessione USB non è attiva.';
@override
String get usbErrorConnectTimedOut =>
'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Dispositivo per comunicazione seriale su rete';
@override
String get usbStatus_notConnected => 'Seleziona un dispositivo USB';
@override
String get usbStatus_connecting => 'Connessione al dispositivo USB...';
@override
String get usbStatus_searching => 'Ricerca di dispositivi USB...';
@override
String usbConnectionFailed(String error) {
return 'Errore nella connessione USB: $error';
}
@override
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
@@ -150,6 +287,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
@override
String get scanner_chromeRequired => 'Browser Chrome richiesto';
@override
String get scanner_chromeRequiredMessage =>
'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.';
@override
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
@@ -233,6 +377,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_longitude => 'Longitudine';
@override
String get settings_contactSettings => 'Impostazioni di contatto';
@override
String get settings_contactSettingsSubtitle =>
'Impostazioni per l\'aggiunta dei contatti';
@override
String get settings_privacyMode => 'Modalità Privacy';
@@ -250,9 +401,60 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
@override
String get settings_privacy => 'Impostazioni sulla privacy';
@override
String get settings_privacySubtitle =>
'Controlla le informazioni che vengono condivise.';
@override
String get settings_privacySettingsDescription =>
'Scegli le informazioni che il tuo dispositivo condivide con gli altri.';
@override
String get settings_denyAll => 'Negare tutto';
@override
String get settings_allowByContact => 'Consenti in base ai flag di contatto';
@override
String get settings_allowAll => 'Consenti tutto';
@override
String get settings_telemetryBaseMode => 'Modalità di base di telemetria';
@override
String get settings_telemetryLocationMode =>
'Modalità di posizionamento telemetrico';
@override
String get settings_telemetryEnvironmentMode =>
'Modalità di ambiente di telemetria';
@override
String get settings_advertLocation => 'Posizione dell\'annuncio';
@override
String get settings_advertLocationSubtitle =>
'Includi la posizione nell\'annuncio';
@override
String get settings_multiAck => 'ACK multipli';
@override
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
@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';
@@ -294,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';
@@ -547,6 +749,50 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotazione del percorso automatico disabilitata';
@override
String get appSettings_maxRouteWeight =>
'Massimo peso consentito per il percorso';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Il peso massimo che un percorso può accumulare grazie a consegne di successo.';
@override
String get appSettings_initialRouteWeight => 'Peso iniziale del percorso';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Peso di partenza per nuovi percorsi';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Aumento del peso del successo';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Peso aggiunto a un percorso dopo una consegna riuscita.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Riduzione del peso associato al fallimento';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Peso rimosso da un percorso dopo un tentativo di consegna fallito.';
@override
String get appSettings_maxMessageRetries =>
'Numero massimo di tentativi di invio del messaggio';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batteria';
@@ -753,6 +999,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
@override
String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato';
@override
String contacts_groupAlreadyExists(String name) {
return 'Il gruppo \"$name\" esiste già.';
@@ -792,6 +1041,42 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Ultimo visto $days giorni fa';
}
@override
String get contact_info => 'Informazioni di Contatto';
@override
String get contact_settings => 'Impostazioni di contatto';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Ultimo accesso';
@override
String get contact_clearChat => 'Cancella chat';
@override
String get contact_teleBase => 'Base di telemetria';
@override
String get contact_teleBaseSubtitle =>
'Consenti la condivisione del livello della batteria e della telemetria di base';
@override
String get contact_teleLoc => 'Posizione telemetria';
@override
String get contact_teleLocSubtitle =>
'Consenti la condivisione dei dati di posizione';
@override
String get contact_teleEnv => 'Ambiente di telemetria';
@override
String get contact_teleEnvSubtitle =>
'Consenti la condivisione dei dati del sensore ambientale';
@override
String get channels_title => 'Canali';
@@ -818,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';
@@ -962,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';
@@ -981,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...';
@@ -1176,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';
@@ -1278,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?';
@@ -1359,9 +1661,30 @@ 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';
@override
String get map_setAsMyLocation => 'Imposta come la mia posizione';
@override
String get map_pinLabel => 'Etichetta PIN';
@@ -1407,6 +1730,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_otherNodes => 'Altri Nodi';
@override
String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
@override
String get map_keyPrefix => 'Prefisso Chiave';
@@ -1422,12 +1748,24 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
@override
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
@override
String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery';
@override
String get map_guessedLocation => 'Località indovinata';
@override
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
@override
String get map_sharedPin => 'Condividi PIN';
@override
String get map_sharedAt => 'Condiviso';
@override
String get map_joinRoom => 'Unisciti alla stanza';
@@ -1440,6 +1778,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_runTrace => 'Esegui Path Trace';
@override
String get map_runTraceWithReturnPath =>
'Tornare indietro sullo stesso percorso';
@override
String get map_removeLast => 'Rimuovi ultimo';
@@ -1717,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';
@@ -1754,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';
@@ -2130,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';
@@ -2918,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';
@@ -3116,4 +3512,311 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Ultimo accesso';
@override
String get contactsSettings_title => 'Impostazioni dei contatti';
@override
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
@override
String get contactsSettings_otherTitle =>
'Altre impostazioni relative ai contatti';
@override
String get contactsSettings_autoAddUsersTitle =>
'Aggiungere utenti automaticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Aggiungere ripetitori automaticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Aggiungere automaticamente i server delle stanze';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Aggiungere automaticamente i sensori';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sostituisci il più vecchio';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
@override
String get discoveredContacts_Title => 'Contatti scoperti';
@override
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
@override
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
@override
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
@override
String get discoveredContacts_addContact => 'Aggiungi contatto';
@override
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
@override
String get discoveredContacts_deleteContact => 'Elimina Contatto';
@override
String get discoveredContacts_deleteContactAll =>
'Eliminare tutti i contatti scoperti';
@override
String get discoveredContacts_deleteContactAllContent =>
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
@override
String get chat_sendCooldown =>
'Si prega di attendere un momento prima di inviare nuovamente.';
@override
String get appSettings_jumpToOldestUnread =>
'Vai al messaggio più vecchio non letto';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Quando si apre una chat con messaggi non letti, scorrete verso l\'alto fino al primo messaggio non letto, invece che al più recente.';
@override
String get appSettings_languageHu => 'Ungherese';
@override
String get appSettings_languageJa => 'Giapponese';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Statistiche per radio e reti';
@override
String get radioStats_screenTitle => 'Statistiche radio';
@override
String get radioStats_notConnected =>
'Connettiti a un dispositivo per visualizzare le statistiche radio.';
@override
String get radioStats_firmwareTooOld =>
'Le statistiche radio richiedono il firmware versione 8 o successiva.';
@override
String get radioStats_waiting => 'In attesa dei dati…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Livello di rumore: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Ultimo valore RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Ultimo SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tempo di trasmissione in diretta (totale): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tempo di trasmissione RX (totale): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Livello di rumore (dBm) misurato su campioni recenti.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Livello di rumore: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Recupero delle statistiche radio…';
@override
String get radioStats_settingsTile => 'Statistiche radio';
@override
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 il PIN';
@override
String get scanner_linuxPairingPinTitle =>
'PIN per l\'accoppiamento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
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';
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+707 -8
View File
@@ -38,9 +38,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_delete => 'Excluir';
@override
String get common_deleteAll => 'Excluir Tudo';
@override
String get common_close => 'Fechar';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Editar';
@@ -108,6 +114,136 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Estabelecer conexão via TCP';
@override
String get tcpHostLabel => 'Endereço IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Porta';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Insira o endereço final e conecte-se.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Conectando a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'É necessário fornecer um endereço IP.';
@override
String get tcpErrorPortInvalid =>
'O valor do porto deve estar entre 1 e 65535.';
@override
String get tcpErrorUnsupported =>
'O protocolo TCP não é suportado nesta plataforma.';
@override
String get tcpErrorTimedOut => 'A conexão TCP expirou.';
@override
String tcpConnectionFailed(String error) {
return 'Falha na conexão TCP: $error';
}
@override
String get usbScreenTitle => 'Conecte via USB';
@override
String get usbScreenSubtitle =>
'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.';
@override
String get usbScreenStatus => 'Selecione um dispositivo USB';
@override
String get usbScreenNote =>
'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.';
@override
String get usbScreenEmptyState =>
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
@override
String get usbErrorPermissionDenied =>
'A permissão para acesso via USB foi negada.';
@override
String get usbErrorDeviceMissing =>
'O dispositivo USB selecionado não está mais disponível.';
@override
String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.';
@override
String get usbErrorBusy =>
'Já existe uma solicitação de conexão USB em andamento.';
@override
String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.';
@override
String get usbErrorOpenFailed =>
'Não foi possível abrir o dispositivo USB selecionado.';
@override
String get usbErrorConnectFailed =>
'Não foi possível conectar ao dispositivo USB selecionado.';
@override
String get usbErrorUnsupported =>
'A comunicação serial via USB não é suportada nesta plataforma.';
@override
String get usbErrorAlreadyActive => 'A conexão USB já está ativa.';
@override
String get usbErrorNoDeviceSelected =>
'Nenhum dispositivo USB foi selecionado.';
@override
String get usbErrorPortClosed => 'A conexão USB não está ativa.';
@override
String get usbErrorConnectTimedOut =>
'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web';
@override
String get usbStatus_notConnected => 'Selecione um dispositivo USB';
@override
String get usbStatus_connecting => 'Conectando ao dispositivo USB...';
@override
String get usbStatus_searching => 'Procurando por dispositivos USB...';
@override
String usbConnectionFailed(String error) {
return 'Falha na conexão USB: $error';
}
@override
String get scanner_scanning => 'Procurando por dispositivos...';
@@ -150,6 +286,13 @@ class AppLocalizationsPt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, ative o Bluetooth para escanear por dispositivos.';
@override
String get scanner_chromeRequired => 'Navegador Chrome necessário';
@override
String get scanner_chromeRequiredMessage =>
'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.';
@override
String get scanner_enableBluetooth => 'Ative o Bluetooth';
@@ -234,6 +377,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Configurações de Contato';
@override
String get settings_contactSettingsSubtitle =>
'Configurações para como os contatos são adicionados';
@override
String get settings_privacyMode => 'Modo de Privacidade';
@@ -251,9 +401,59 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
@override
String get settings_privacy => 'Configurações de Privacidade';
@override
String get settings_privacySubtitle => 'Controle o que é compartilhado.';
@override
String get settings_privacySettingsDescription =>
'Escolha quais informações o seu dispositivo compartilha com os outros.';
@override
String get settings_denyAll => 'Negar todos';
@override
String get settings_allowByContact => 'Permitir por bandeiras de contato';
@override
String get settings_allowAll => 'Permitir todos';
@override
String get settings_telemetryBaseMode => 'Modo Base de Telemetria';
@override
String get settings_telemetryLocationMode =>
'Modo de Localização de Telemetria';
@override
String get settings_telemetryEnvironmentMode =>
'Modo de Ambiente de Telemetria';
@override
String get settings_advertLocation => 'Localização do Anúncio';
@override
String get settings_advertLocationSubtitle =>
'Incluir localização no anúncio';
@override
String get settings_multiAck => 'Multi-ACKs';
@override
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
@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';
@@ -296,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';
@@ -549,6 +749,49 @@ class AppLocalizationsPt extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotação de roteamento automático desativada';
@override
String get appSettings_maxRouteWeight => 'Peso Máximo da Rota';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.';
@override
String get appSettings_initialRouteWeight => 'Peso Inicial da Rota';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Peso inicial para novos caminhos descobertos';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Aumento do peso para indicar sucesso';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Peso adicionado a um caminho após a entrega bem-sucedida.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Redução do peso da falha';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Peso removido de um caminho após uma tentativa de entrega malsucedida.';
@override
String get appSettings_maxMessageRetries =>
'Número máximo de tentativas de envio de mensagens';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Número de tentativas de reenvio antes de classificar uma mensagem como falha.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Bateria';
@@ -756,6 +999,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
@override
String get contacts_groupNameReserved => 'Este nome de grupo está reservado';
@override
String contacts_groupAlreadyExists(String name) {
return 'O grupo \"$name\" já existe';
@@ -795,6 +1041,42 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Última vez visto $days dias atrás';
}
@override
String get contact_info => 'Informações de Contato';
@override
String get contact_settings => 'Configurações de Contato';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Visto pela última vez';
@override
String get contact_clearChat => 'Limpar Chat';
@override
String get contact_teleBase => 'Base de Telemetria';
@override
String get contact_teleBaseSubtitle =>
'Permitir compartilhamento do nível da bateria e telemetria básica';
@override
String get contact_teleLoc => 'Localização de Telemetria';
@override
String get contact_teleLocSubtitle =>
'Permitir compartilhamento de dados de localização';
@override
String get contact_teleEnv => 'Ambiente de Telemetria';
@override
String get contact_teleEnvSubtitle =>
'Permitir compartilhamento de dados do sensor de ambiente';
@override
String get channels_title => 'Canais';
@@ -821,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';
@@ -964,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';
@@ -983,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...';
@@ -1178,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';
@@ -1279,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?';
@@ -1361,9 +1660,30 @@ 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';
@override
String get map_setAsMyLocation => 'Defina minha localização';
@override
String get map_pinLabel => 'Rótulo de marcador';
@@ -1409,6 +1729,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_otherNodes => 'Outros Nós';
@override
String get map_showOverlaps => 'Sobreposições da Chave Repeater';
@override
String get map_keyPrefix => 'Prefixo Chave';
@@ -1424,12 +1747,25 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
@override
String get map_showGuessedLocations =>
'Mostrar as localizações dos nós estimados';
@override
String get map_showDiscoveryContacts => 'Mostrar Contatos de Descoberta';
@override
String get map_guessedLocation => 'Localização estimada';
@override
String get map_lastSeenTime => 'Último Tempo de Visualização';
@override
String get map_sharedPin => 'Pin compartilhado';
@override
String get map_sharedAt => 'Compartilhado';
@override
String get map_joinRoom => 'Junte-se à Sala';
@@ -1442,6 +1778,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_runTrace => 'Executar Traçado de Caminho';
@override
String get map_runTraceWithReturnPath => 'Retornar ao mesmo caminho.';
@override
String get map_removeLast => 'Remover Último';
@@ -1719,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';
@@ -1754,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';
@@ -2130,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';
@@ -2917,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';
@@ -3111,4 +3504,310 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Visto pela última vez';
@override
String get contactsSettings_title => 'Configurações de contatos';
@override
String get contactsSettings_autoAddTitle => 'Descoberta Automática';
@override
String get contactsSettings_otherTitle =>
'Outras configurações relacionadas a contatos';
@override
String get contactsSettings_autoAddUsersTitle =>
'Adicionar usuários automaticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Permitir que o companheiro adicione automaticamente os usuários descobertos.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Adicionar repetidores automaticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Permitir que o companheiro adicione automaticamente os repetidores descobertos.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Adicionar automaticamente servidores de sala';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Adicionar sensores automaticamente';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Permitir que o companheiro adicione automaticamente sensores descobertos.';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sobrescrever o Mais Antigo';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.';
@override
String get discoveredContacts_Title => 'Contatos Descobertos';
@override
String get discoveredContacts_noMatching => 'Nenhum contato correspondente';
@override
String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos';
@override
String get discoveredContacts_contactAdded => 'Contato adicionado';
@override
String get discoveredContacts_addContact => 'Adicionar Contato';
@override
String get discoveredContacts_copyContact =>
'Copiar Contato para a área de transferência';
@override
String get discoveredContacts_deleteContact => 'Excluir Contato';
@override
String get discoveredContacts_deleteContactAll =>
'Excluir Todos os Contatos Descobertos';
@override
String get discoveredContacts_deleteContactAllContent =>
'Tem certeza de que deseja excluir todos os contatos descobertos?';
@override
String get chat_sendCooldown =>
'Por favor, aguarde um momento antes de reenviar.';
@override
String get appSettings_jumpToOldestUnread =>
'Vá para a mensagem mais antiga não lida';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.';
@override
String get appSettings_languageHu => 'Húngaro';
@override
String get appSettings_languageJa => 'Japonês';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Estatísticas de rádio e malha';
@override
String get radioStats_screenTitle => 'Estatísticas de rádio';
@override
String get radioStats_notConnected =>
'Conecte-se a um dispositivo para visualizar estatísticas de rádio.';
@override
String get radioStats_firmwareTooOld =>
'As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.';
@override
String get radioStats_waiting => 'Aguardando dados…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Nível de ruído: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Último RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Último SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tempo de transmissão da TX (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tempo de uso do RX (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Nível de ruído (dBm) em amostras recentes.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Nível de ruído: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Obtendo estatísticas de rádio…';
@override
String get radioStats_settingsTile => 'Estatísticas de rádio';
@override
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';
@override
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN de emparelhamento Bluetooth';
@override
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';
}
+710 -8
View File
@@ -38,9 +38,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get common_delete => 'Удалить';
@override
String get common_deleteAll => 'Удалить все';
@override
String get common_close => 'Закрыть';
@override
String get common_done => 'Готово';
@override
String get common_edit => 'Изменить';
@@ -108,6 +114,137 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Установить соединение по протоколу TCP';
@override
String get tcpHostLabel => 'IP-адрес';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Порт';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Введите адрес и подключитесь.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Подключение к $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необходимо указать IP-адрес.';
@override
String get tcpErrorPortInvalid =>
'Порт должен находиться в диапазоне от 1 до 65535.';
@override
String get tcpErrorUnsupported =>
'Протокол TCP не поддерживается на этой платформе.';
@override
String get tcpErrorTimedOut => 'Соединение TCP не удалось установить.';
@override
String tcpConnectionFailed(String error) {
return 'Не удалось установить соединение TCP: $error';
}
@override
String get usbScreenTitle => 'Подключение через USB';
@override
String get usbScreenSubtitle =>
'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.';
@override
String get usbScreenStatus => 'Выберите USB-устройство';
@override
String get usbScreenNote =>
'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.';
@override
String get usbScreenEmptyState =>
'Не обнаружено устройств USB. Подключите одно из них и обновите список.';
@override
String get usbErrorPermissionDenied =>
'Запрос на доступ через USB был отклонен.';
@override
String get usbErrorDeviceMissing =>
'Выбранное USB-устройство больше недоступно.';
@override
String get usbErrorInvalidPort => 'Выберите действительное USB-устройство.';
@override
String get usbErrorBusy =>
'Еще одно запрошенное соединение через USB уже находится в процессе.';
@override
String get usbErrorNotConnected => 'Ни одно USB-устройство не подключено.';
@override
String get usbErrorOpenFailed =>
'Не удалось открыть выбранное USB-устройство.';
@override
String get usbErrorConnectFailed =>
'Не удалось установить соединение с выбранным USB-устройством.';
@override
String get usbErrorUnsupported =>
'Поддержка последовательного USB отсутствует на данной платформе.';
@override
String get usbErrorAlreadyActive => 'USB-соединение уже установлено.';
@override
String get usbErrorNoDeviceSelected =>
'Не было выбрано ни одно устройство USB.';
@override
String get usbErrorPortClosed => 'USB-соединение не установлено.';
@override
String get usbErrorConnectTimedOut =>
'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.';
@override
String get usbFallbackDeviceName =>
'Устройство для последовательного подключения к сети';
@override
String get usbStatus_notConnected => 'Выберите USB-устройство';
@override
String get usbStatus_connecting => 'Подключение к USB-устройству...';
@override
String get usbStatus_searching => 'Поиск USB-устройств...';
@override
String usbConnectionFailed(String error) {
return 'Не удалось установить соединение через USB: $error';
}
@override
String get scanner_scanning => 'Поиск устройств...';
@@ -149,6 +286,13 @@ class AppLocalizationsRu extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
@override
String get scanner_chromeRequired => 'Требуется браузер Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.';
@override
String get scanner_enableBluetooth => 'Включите Bluetooth';
@@ -232,6 +376,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_longitude => 'Долгота';
@override
String get settings_contactSettings => 'Настройки контактов';
@override
String get settings_contactSettingsSubtitle =>
'Настройки добавления контактов';
@override
String get settings_privacyMode => 'Режим конфиденциальности';
@@ -250,9 +401,59 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Режим конфиденциальности выключен';
@override
String get settings_privacy => 'Настройки конфиденциальности';
@override
String get settings_privacySubtitle =>
'Контролируйте, какую информацию делиться.';
@override
String get settings_privacySettingsDescription =>
'Выберите, какую информацию ваше устройство будет делиться с другими.';
@override
String get settings_denyAll => 'Отклонить все';
@override
String get settings_allowByContact => 'Разрешить по флагам контактов';
@override
String get settings_allowAll => 'Разрешить все';
@override
String get settings_telemetryBaseMode => 'Базовый режим телеметрии';
@override
String get settings_telemetryLocationMode =>
'Режим местоположения телеметрии';
@override
String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии';
@override
String get settings_advertLocation => 'Местоположение рекламы';
@override
String get settings_advertLocationSubtitle =>
'Включить местоположение в объявление';
@override
String get settings_multiAck => 'Несколько подтверждений';
@override
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
@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 => 'Отправить анонсирование';
@@ -294,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 => 'Журнал отладки приложения';
@@ -548,6 +749,50 @@ class AppLocalizationsRu extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Автоматическое переключение маршрутов отключено';
@override
String get appSettings_maxRouteWeight =>
'Максимальный допустимый вес маршрута';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.';
@override
String get appSettings_initialRouteWeight => 'Начальный вес маршрута';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Начальный вес для новых, только что открытых маршрутов';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Увеличение веса успеха';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Вес, добавленный к маршруту после успешной доставки.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Уменьшение веса неудачи';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Вес, который был удален с пути после неудачной доставки.';
@override
String get appSettings_maxMessageRetries =>
'Максимальное количество повторных попыток отправки сообщения';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батарея';
@@ -754,6 +999,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Имя группы обязательно';
@override
String get contacts_groupNameReserved => 'Это имя группы зарезервировано';
@override
String contacts_groupAlreadyExists(String name) {
return 'Группа \"$name\" уже существует';
@@ -793,6 +1041,42 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Видели $days дн. назад';
}
@override
String get contact_info => 'Контактная информация';
@override
String get contact_settings => 'Настройки контактов';
@override
String get contact_telemetry => 'Телеметрия';
@override
String get contact_lastSeen => 'Последний раз видели';
@override
String get contact_clearChat => 'Очистить чат';
@override
String get contact_teleBase => 'База телеметрии';
@override
String get contact_teleBaseSubtitle =>
'Разрешить обмен уровнем заряда батареи и базовой телеметрией';
@override
String get contact_teleLoc => 'Местоположение телеметрии';
@override
String get contact_teleLocSubtitle =>
'Разрешить обмен данными о местоположении';
@override
String get contact_teleEnv => 'Среда телеметрии';
@override
String get contact_teleEnvSubtitle =>
'Разрешить обмен данными датчиков окружающей среды';
@override
String get channels_title => 'Каналы';
@@ -819,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 => 'Приватный';
@@ -962,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 => 'Отправьте сообщение, чтобы начать';
@@ -981,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 => 'Напишите сообщение...';
@@ -1179,6 +1471,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_successes => 'успешно';
@override
String get chat_score => 'Оценка';
@override
String get chat_removePath => 'Удалить маршрут';
@@ -1281,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 => 'Открыть ссылку?';
@@ -1362,9 +1663,30 @@ 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 => 'Поделиться меткой здесь';
@override
String get map_setAsMyLocation => 'Установить мое местоположение';
@override
String get map_pinLabel => 'Метка';
@@ -1410,6 +1732,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_otherNodes => 'Другие ноды';
@override
String get map_showOverlaps => 'Перекрытия ключа повтора';
@override
String get map_keyPrefix => 'Префикс ключа';
@@ -1425,12 +1750,25 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Показывать общие метки';
@override
String get map_showGuessedLocations =>
'Отобразить предполагаемые места расположения узлов';
@override
String get map_showDiscoveryContacts => 'Показать контакты Discovery';
@override
String get map_guessedLocation => 'Угаданное место';
@override
String get map_lastSeenTime => 'Время последнего появления';
@override
String get map_sharedPin => 'Общая метка';
@override
String get map_sharedAt => 'Поделено';
@override
String get map_joinRoom => 'Присоединиться к комнате';
@@ -1443,6 +1781,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_runTrace => 'Запустить трассировку пути';
@override
String get map_runTraceWithReturnPath => 'Вернуться обратно по тому же пути';
@override
String get map_removeLast => 'Удалить последний';
@@ -1721,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 => 'Статус';
@@ -1756,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 => 'Статус репитера';
@@ -2132,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 => 'Отправляет пакет анонсирования';
@@ -2920,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 => 'Частота';
@@ -3123,4 +3519,310 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Последний раз видели';
@override
String get contactsSettings_title => 'Настройки контактов';
@override
String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение';
@override
String get contactsSettings_otherTitle =>
'Другие настройки, связанные с контактами';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматически добавлять пользователей';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженных пользователей';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматически добавлять ретрансляторы';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматически добавлять серверы комнат';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматически добавлять датчики';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженные датчики';
@override
String get contactsSettings_overwriteOldestTitle =>
'Перезаписать самое старое';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.';
@override
String get discoveredContacts_Title => 'Обнаруженные контакты';
@override
String get discoveredContacts_noMatching => 'Нет совпадающих контактов';
@override
String get discoveredContacts_searchHint => 'Найденные контакты поиска';
@override
String get discoveredContacts_contactAdded => 'Контакт добавлен';
@override
String get discoveredContacts_addContact => 'Добавить контакт';
@override
String get discoveredContacts_copyContact =>
'Копировать контакт в буфер обмена';
@override
String get discoveredContacts_deleteContact => 'Удалить контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Удалить Все Обнаруженные Контакты';
@override
String get discoveredContacts_deleteContactAllContent =>
'Вы уверены, что хотите удалить все обнаруженные контакты?';
@override
String get chat_sendCooldown =>
'Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.';
@override
String get appSettings_jumpToOldestUnread =>
'Перейти к самому старому непрочитанному сообщению';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.';
@override
String get appSettings_languageHu => 'Венгерский';
@override
String get appSettings_languageJa => 'Японский';
@override
String get appSettings_languageKo => 'Корейский';
@override
String get radioStats_tooltip => 'Статистика радио и беспроводной сети';
@override
String get radioStats_screenTitle => 'Статистика радиовещания';
@override
String get radioStats_notConnected =>
'Подключитесь к устройству, чтобы просмотреть статистику радио.';
@override
String get radioStats_firmwareTooOld =>
'Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.';
@override
String get radioStats_waiting => 'Ожидаем данных…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Уровень шума: $noiseDbm дБм';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Последнее значение RSSI: $rssiDbm дБм';
}
@override
String radioStats_lastSnr(String snr) {
return 'Последнее значение SNR: $snr дБ';
}
@override
String radioStats_txAir(int seconds) {
return 'Время эфира на телеканале TX (общее): $seconds секунд';
}
@override
String radioStats_rxAir(int seconds) {
return 'Общее время использования RX (в секундах): $seconds с';
}
@override
String get radioStats_chartCaption =>
'Уровень шума (дБм) на основе последних измерений.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Уровень шума: $noiseDbm дБм';
}
@override
String get radioStats_stripWaiting => 'Получение данных о радио…';
@override
String get radioStats_settingsTile => 'Статистика радиовещания';
@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 => 'Ссылка на руководство';
@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';
@override
String get scanner_linuxPairingPinTitle => 'PIN‑код сопряжения Bluetooth';
@override
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 => 'Неизвестно';
}
+700 -8
View File
@@ -38,9 +38,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_delete => 'Odstrániť';
@override
String get common_deleteAll => 'Zmazať všetko';
@override
String get common_close => 'Zavrieť';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Upraviť';
@@ -108,6 +114,135 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Spojte sa pomocou protokolu TCP';
@override
String get tcpHostLabel => 'IP adresa';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Zadajte cieľovú adresu a pripojte sa.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Pripojenie k $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Je potrebné zadať IP adresu.';
@override
String get tcpErrorPortInvalid => 'Číslo portu musí byť medzi 1 a 65535.';
@override
String get tcpErrorUnsupported =>
'Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.';
@override
String get tcpErrorTimedOut => 'Pripojenie TCP vypršalo.';
@override
String tcpConnectionFailed(String error) {
return 'Neúspešné vytvorenie TCP spojenia: $error';
}
@override
String get usbScreenTitle => 'Pripojte cez USB';
@override
String get usbScreenSubtitle =>
'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.';
@override
String get usbScreenStatus => 'Vyberte USB zariadenie';
@override
String get usbScreenNote =>
'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.';
@override
String get usbScreenEmptyState =>
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
@override
String get usbErrorPermissionDenied =>
'Žiadosť o prístup cez USB bola zamietnutá.';
@override
String get usbErrorDeviceMissing =>
'Vybrané USB zariadenie už nie je dostupné.';
@override
String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.';
@override
String get usbErrorBusy =>
'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.';
@override
String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.';
@override
String get usbErrorOpenFailed =>
'Nepodarilo sa otvoriť vybrané USB zariadenie.';
@override
String get usbErrorConnectFailed =>
'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.';
@override
String get usbErrorUnsupported =>
'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.';
@override
String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.';
@override
String get usbErrorNoDeviceSelected =>
'Nebolo vybrané žiadne USB zariadenie.';
@override
String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.';
@override
String get usbErrorConnectTimedOut =>
'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Webový sériový zariadenie';
@override
String get usbStatus_notConnected => 'Vyberte USB zariadenie';
@override
String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...';
@override
String get usbStatus_searching => 'Hľadanie USB zariadení...';
@override
String usbConnectionFailed(String error) {
return 'Neúspešné pripojenie cez USB: $error';
}
@override
String get scanner_scanning => 'Skrívania zariadení...';
@@ -150,6 +285,13 @@ class AppLocalizationsSk extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
@override
String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.';
@override
String get scanner_enableBluetooth => 'Povolte Bluetooth';
@@ -233,6 +375,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_longitude => 'Dĺžka';
@override
String get settings_contactSettings => 'Nastavenia kontaktov';
@override
String get settings_contactSettingsSubtitle =>
'Nastavenia pre pridávanie kontaktov.';
@override
String get settings_privacyMode => 'Režim ochrany súkromia';
@@ -249,9 +398,57 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
@override
String get settings_privacy => 'Nastavenia súkromia';
@override
String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.';
@override
String get settings_privacySettingsDescription =>
'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.';
@override
String get settings_denyAll => 'Zamietnuť všetko';
@override
String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok';
@override
String get settings_allowAll => 'Povoliť všetko';
@override
String get settings_telemetryBaseMode => 'Základný režim telemetrie';
@override
String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie';
@override
String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie';
@override
String get settings_advertLocation => 'Umiestnenie inzerátu';
@override
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
@override
String get settings_multiAck => 'Viaceré ACK';
@override
String get settings_telemetryModeUpdated =>
'Režim telemetrie bol aktualizovaný';
@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';
@@ -293,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';
@@ -541,6 +738,48 @@ class AppLocalizationsSk extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatické prekladanie trás pozastavené';
@override
String get appSettings_maxRouteWeight => 'Maximálna hmotnosť trasy';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.';
@override
String get appSettings_initialRouteWeight => 'Počiatočná váha trasy';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Počiatočná váha pre nové, objavené cesty';
@override
String get appSettings_routeWeightSuccessIncrement => 'Zvyšenie váhy úspechu';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Hmotnosť pridaná k trase po úspešnej doručení';
@override
String get appSettings_routeWeightFailureDecrement =>
'Sníženie váhy, ktorá sa používa na odhad rizika.';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Hmotnosť odstránená z cesty po neúspešnej doručenie';
@override
String get appSettings_maxMessageRetries =>
'Maximalný počet pokusov o doručenie správ';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Počet pokusov o odošleť pred označením správy ako neúspešnej';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batéria';
@@ -748,6 +987,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
@override
String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný';
@override
String contacts_groupAlreadyExists(String name) {
return 'Skupina \"$name\" už existuje';
@@ -789,6 +1031,41 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Posledné zobrazenie $days dní dozadu';
}
@override
String get contact_info => 'Kontaktné informácie';
@override
String get contact_settings => 'Nastavenia kontaktov';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Naposledy videný';
@override
String get contact_clearChat => 'Vymazať chat';
@override
String get contact_teleBase => 'Báza telemetrie';
@override
String get contact_teleBaseSubtitle =>
'Povoliť zdieľanie úrovne batérie a základnej telemetrie';
@override
String get contact_teleLoc => 'Lokácia telemetrie';
@override
String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite';
@override
String get contact_teleEnv => 'Prostredie telemetrie';
@override
String get contact_teleEnvSubtitle =>
'Povoliť zdieľanie údajov senzorov prostredia';
@override
String get channels_title => 'Kanály';
@@ -815,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é';
@@ -957,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';
@@ -976,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...';
@@ -1173,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';
@@ -1274,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?';
@@ -1355,9 +1649,30 @@ 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';
@override
String get map_setAsMyLocation => 'Nastavte ako moju polohu';
@override
String get map_pinLabel => 'Označka upozornenia';
@@ -1403,6 +1718,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_otherNodes => 'Ostatné uzly';
@override
String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
@override
String get map_keyPrefix => 'Päťciferné predpona';
@@ -1418,12 +1736,25 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
@override
String get map_showGuessedLocations =>
'Zobraziť umiestnenia odhadnutých uzlov';
@override
String get map_showDiscoveryContacts => 'Zobraziť kontakty objavov';
@override
String get map_guessedLocation => 'Odhadnutá lokalita';
@override
String get map_lastSeenTime => 'Posledný čas sledovania';
@override
String get map_sharedPin => 'Zdieľaný PIN';
@override
String get map_sharedAt => 'Zdieľané';
@override
String get map_joinRoom => 'Pripojiť miestnosť';
@@ -1436,6 +1767,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_runTrace => 'Spustiť trasovaním cesty';
@override
String get map_runTraceWithReturnPath => 'Vráťte sa späť po tej istej ceste.';
@override
String get map_removeLast => 'Odstrániť posledný';
@@ -1713,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';
@@ -1748,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';
@@ -2118,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.';
@@ -2902,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';
@@ -3098,4 +3486,308 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Naposledy videný';
@override
String get contactsSettings_title => 'Nastavenia kontaktov';
@override
String get contactsSettings_autoAddTitle => 'Automatické zisťovanie';
@override
String get contactsSettings_otherTitle =>
'Ďalšie nastavenia súvisiace s kontaktami';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automaticky pridávať užívateľov';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automaticky pridávať opakovače';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavené repeater.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automaticky pridávať server miestnosti';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automaticky pridávať senzory';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavené senzory.';
@override
String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.';
@override
String get discoveredContacts_Title => 'Objavené kontakty';
@override
String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty';
@override
String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty';
@override
String get discoveredContacts_contactAdded => 'Kontakt bol pridaný';
@override
String get discoveredContacts_addContact => 'Pridať kontakt';
@override
String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky';
@override
String get discoveredContacts_deleteContact => 'Zmazať kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Zmazať všetky objavené kontakty';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
@override
String get chat_sendCooldown => 'Prosím, počkajte chvíľu, než zašlete znova.';
@override
String get appSettings_jumpToOldestUnread => 'Presk oceň';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.';
@override
String get appSettings_languageHu => 'Maďarský';
@override
String get appSettings_languageJa => 'Japonský';
@override
String get appSettings_languageKo => 'Kórejský';
@override
String get radioStats_tooltip => 'Statistiky rádiových a sieťových kanálov';
@override
String get radioStats_screenTitle => 'Štatistiky rádiových vysielaní';
@override
String get radioStats_notConnected =>
'Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.';
@override
String get radioStats_firmwareTooOld =>
'Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.';
@override
String get radioStats_waiting => 'Čakám na údaje…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Úroveň hluku: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Posledný údaj RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Posledná hodnota SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Čas vysielania na TX (celkový): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Čas RX (celkový): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Úroveň šumu (dBm) pre posledné vzorky.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Úroveň hluku: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Získavanie údajov o rádiu…';
@override
String get radioStats_settingsTile => 'Štatistiky rádiových vysielaní';
@override
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';
@override
String get scanner_linuxPairingHidePin => 'Skryť PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
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';
}
+699 -8
View File
@@ -38,9 +38,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_delete => 'Izbrisati';
@override
String get common_deleteAll => 'Izbriši vse';
@override
String get common_close => 'Zapri';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Uredi';
@@ -108,6 +114,133 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Komunicirajte preko protokola TCP';
@override
String get tcpHostLabel => 'IP naslov';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Vrata';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Vnesite končni naslov in se povežite';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Povezava z $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Potrebna je IP-naslov.';
@override
String get tcpErrorPortInvalid => 'Port mora biti med 1 in 65535.';
@override
String get tcpErrorUnsupported =>
'Transport preko protokola TCP ni podprt na tej platformi.';
@override
String get tcpErrorTimedOut => 'Povezava TCP je presegla časovno obdobje.';
@override
String tcpConnectionFailed(String error) {
return 'Napaka pri povezavi TCP: $error';
}
@override
String get usbScreenTitle => 'Povežite preko USB';
@override
String get usbScreenSubtitle =>
'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.';
@override
String get usbScreenStatus => 'Izberite USB naprave';
@override
String get usbScreenNote =>
'USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.';
@override
String get usbScreenEmptyState =>
'Niti en USB naprave niso najdeni. Povežite eno in posodobite.';
@override
String get usbErrorPermissionDenied =>
'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.';
@override
String get usbErrorDeviceMissing => 'Izbrani USB napravej je več ne.';
@override
String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.';
@override
String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.';
@override
String get usbErrorNotConnected => 'Ni priklopljenih USB naprave.';
@override
String get usbErrorOpenFailed =>
'Uspešno ni bilo mogo, da se odpre izbran naprave USB.';
@override
String get usbErrorConnectFailed =>
'Niso bilo mogoče uskladiti povezave z izbranim USB napom.';
@override
String get usbErrorUnsupported =>
'USB serijska komunikacija ni podprta na tej platformi.';
@override
String get usbErrorAlreadyActive => 'USB povezava je že aktivirana.';
@override
String get usbErrorNoDeviceSelected => 'Ni bilo izbranega USB naprave.';
@override
String get usbErrorPortClosed => 'USB povezava ni aktivirana.';
@override
String get usbErrorConnectTimedOut =>
'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Naprave za serijsko komunikacijo preko spleta';
@override
String get usbStatus_notConnected => 'Izberite USB naprave.';
@override
String get usbStatus_connecting => 'Povezava z USB napravo...';
@override
String get usbStatus_searching => 'Iskanje USB naprav...';
@override
String usbConnectionFailed(String error) {
return 'Napaka pri povezavi preko USB: $error';
}
@override
String get scanner_scanning => 'Skeniram za naprave...';
@@ -150,6 +283,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
@override
String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.';
@override
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
@@ -233,6 +373,13 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_longitude => 'Dolžina';
@override
String get settings_contactSettings => 'Nastavitve stika';
@override
String get settings_contactSettingsSubtitle =>
'Nastavitve za dodajanje stikov.';
@override
String get settings_privacyMode => 'Zasebnost';
@@ -249,9 +396,58 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
@override
String get settings_privacy => 'Nastavitve zasebnosti';
@override
String get settings_privacySubtitle =>
'Kontrolirajte, katere informacije so deljene.';
@override
String get settings_privacySettingsDescription =>
'Izberite, katere informacije vaš naprava deli z drugimi.';
@override
String get settings_denyAll => 'Zavrniti vse';
@override
String get settings_allowByContact => 'Dovoli po kontaktnih zastavah';
@override
String get settings_allowAll => 'Dovoli vse';
@override
String get settings_telemetryBaseMode => 'Osnovni način telemetrije';
@override
String get settings_telemetryLocationMode => 'Način delovanja telemetrije';
@override
String get settings_telemetryEnvironmentMode =>
'Način delovanja okolja telemetrije';
@override
String get settings_advertLocation => 'Lokacija oglasa';
@override
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
@override
String get settings_multiAck => 'Več potrdil';
@override
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
@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';
@@ -292,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';
@@ -543,6 +739,49 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Samodejno krmilno rotiranje je onemogočeno';
@override
String get appSettings_maxRouteWeight => 'Največja dovoljena teža poti';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.';
@override
String get appSettings_initialRouteWeight => 'Izvirna teža poti';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Izguba teže za nove, odkriti poti';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Učinkovitost: povečanje';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Težava, dodana poti po uspešni dostavi';
@override
String get appSettings_routeWeightFailureDecrement =>
'Zmanjšanje teže, ki je povezana s pomanjkanjem';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Težo, ki ni bila uspešno dostavljena, odstranili s poti.';
@override
String get appSettings_maxMessageRetries =>
'Najve število poskusov pošiljanja sporočil';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Baterija';
@@ -748,6 +987,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
@override
String get contacts_groupNameReserved => 'To ime skupine je rezervirano';
@override
String contacts_groupAlreadyExists(String name) {
return 'Skupina \"$name\" že obstaja';
@@ -787,6 +1029,41 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Zadnjič viden pred $days dnem';
}
@override
String get contact_info => 'Kontaktni podatki';
@override
String get contact_settings => 'Nastavitve stika';
@override
String get contact_telemetry => 'Telemetrija';
@override
String get contact_lastSeen => 'Zadnjič videno';
@override
String get contact_clearChat => 'Počisti klepet';
@override
String get contact_teleBase => 'Baza telemetrije';
@override
String get contact_teleBaseSubtitle =>
'Dovoli deljenje stanja baterije in osnovne telemetrije';
@override
String get contact_teleLoc => 'Lokacija telemetrije';
@override
String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji';
@override
String get contact_teleEnv => 'Okolje telemetrije';
@override
String get contact_teleEnvSubtitle =>
'Dovoli deljenje podatkov okoljskih senzorjev';
@override
String get channels_title => 'Kanali';
@@ -813,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';
@@ -955,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.';
@@ -975,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...';
@@ -1170,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';
@@ -1269,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?';
@@ -1351,9 +1645,30 @@ 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.';
@override
String get map_setAsMyLocation => 'Nastavite to kot mojo lokacijo';
@override
String get map_pinLabel => 'Oznaka za pritrditev';
@@ -1399,6 +1714,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_otherNodes => 'Druge vozlišča';
@override
String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
@override
String get map_keyPrefix => 'Predpona ključa';
@@ -1414,12 +1732,24 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Pokaži skupno označenja';
@override
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
@override
String get map_showDiscoveryContacts => 'Prikaži odkritja kontaktov';
@override
String get map_guessedLocation => 'Predpostavljena lokacija';
@override
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
@override
String get map_sharedPin => 'Deljeno naslovno geslo';
@override
String get map_sharedAt => 'Deljeno';
@override
String get map_joinRoom => 'Pridružiti sobo';
@@ -1432,6 +1762,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_runTrace => 'Zaženi sledenje poti';
@override
String get map_runTraceWithReturnPath => 'Vrni se nazaj po isti poti.';
@override
String get map_removeLast => 'Odstrani Zadnji';
@@ -1711,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';
@@ -1748,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';
@@ -2122,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';
@@ -2905,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';
@@ -3103,4 +3489,309 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Zadnjič videno';
@override
String get contactsSettings_title => 'Nastavitve stikov';
@override
String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje';
@override
String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki';
@override
String get contactsSettings_autoAddUsersTitle =>
'Avtomatsko dodaj uporabnike';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Avtomatsko dodaj ponovitelje';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Avtomatsko dodaj strežnike sob';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Avtomatsko dodaj senzorje';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.';
@override
String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.';
@override
String get discoveredContacts_Title => 'Odkriti stiki';
@override
String get discoveredContacts_noMatching => 'Ni ujemajočih stikov';
@override
String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju';
@override
String get discoveredContacts_contactAdded => 'Kontakt dodan';
@override
String get discoveredContacts_addContact => 'Dodaj stik';
@override
String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče';
@override
String get discoveredContacts_deleteContact => 'Izbriši stik';
@override
String get discoveredContacts_deleteContactAll =>
'Izbriši vse odkrite kontakte';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
@override
String get chat_sendCooldown =>
'Prosimo, počakajte trenutek, preden pošljete ponovno.';
@override
String get appSettings_jumpToOldestUnread =>
'Pritisnite za najstarejše nepročitano sporočilo';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.';
@override
String get appSettings_languageHu => 'Madžarski';
@override
String get appSettings_languageJa => 'Japonski';
@override
String get appSettings_languageKo => 'Korejski';
@override
String get radioStats_tooltip => 'Statistike za radio in mrežo';
@override
String get radioStats_screenTitle => 'Radijske statistike';
@override
String get radioStats_notConnected =>
'Povežite se z napravo, da si ogledate statistiko o radiju.';
@override
String get radioStats_firmwareTooOld =>
'Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.';
@override
String get radioStats_waiting => 'Čakam na podatke…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Število šuma: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Najkasnejše vrednost RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Najkasnejše vrednost SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Čas na TX (skupno): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Čas, namenjen RX-ju (skupno): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ravnovredna raven šuma (dBm) za nedavne vzorce.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Število šuma: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Prejemanje statistike o radiju…';
@override
String get radioStats_settingsTile => 'Radijske statistike';
@override
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';
@override
String get scanner_linuxPairingHidePin => 'Skrij PIN';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth PIN za seznanjanje';
@override
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';
}
+699 -7
View File
@@ -38,9 +38,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_delete => 'Radera';
@override
String get common_deleteAll => 'Ta bort alla';
@override
String get common_close => 'Stänga';
@override
String get common_done => 'Done';
@override
String get common_edit => 'Redigera';
@@ -108,6 +114,133 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Anslut via TCP';
@override
String get tcpHostLabel => 'IP-adress';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Ange slutpunkt och anslut';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Anslutning till $endpoint...';
}
@override
String get tcpErrorHostRequired => 'IP-adress krävs.';
@override
String get tcpErrorPortInvalid => 'Porten måste vara mellan 1 och 65535.';
@override
String get tcpErrorUnsupported =>
'TCP-transport fungerar inte på denna plattform.';
@override
String get tcpErrorTimedOut => 'TCP-anslutningen har tidsut gått.';
@override
String tcpConnectionFailed(String error) {
return 'Fel vid TCP-anslutning: $error';
}
@override
String get usbScreenTitle => 'Anslut via USB';
@override
String get usbScreenSubtitle =>
'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.';
@override
String get usbScreenStatus => 'Välj en USB-enhet';
@override
String get usbScreenNote =>
'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.';
@override
String get usbScreenEmptyState =>
'Inga USB-enheter hittades. Anslut en och uppdatera.';
@override
String get usbErrorPermissionDenied => 'Tillgången via USB nekas.';
@override
String get usbErrorDeviceMissing =>
'Den valda USB-enheten är inte längre tillgänglig.';
@override
String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.';
@override
String get usbErrorBusy =>
'En annan förfrågan om USB-anslutning är redan pågående.';
@override
String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.';
@override
String get usbErrorOpenFailed =>
'Misslyckades med att öppna det valda USB-enheten.';
@override
String get usbErrorConnectFailed =>
'Kunde inte ansluta till det valda USB-enheten.';
@override
String get usbErrorUnsupported =>
'USB-seriell kommunikation stöds inte på denna plattform.';
@override
String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.';
@override
String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.';
@override
String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.';
@override
String get usbErrorConnectTimedOut =>
'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.';
@override
String get usbFallbackDeviceName => 'Web-serieenhet';
@override
String get usbStatus_notConnected => 'Välj en USB-enhet';
@override
String get usbStatus_connecting => 'Anslutning till USB-enhet...';
@override
String get usbStatus_searching => 'Söker efter USB-enheter...';
@override
String usbConnectionFailed(String error) {
return 'Fel vid USB-anslutning: $error';
}
@override
String get scanner_scanning => 'Söker efter enheter...';
@@ -149,6 +282,13 @@ class AppLocalizationsSv extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Vänligen aktivera Bluetooth för att söka efter enheter.';
@override
String get scanner_chromeRequired => 'Chrome-webbläsare krävs';
@override
String get scanner_chromeRequiredMessage =>
'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.';
@override
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
@@ -232,6 +372,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_longitude => 'Längdgrad';
@override
String get settings_contactSettings => 'Kontaktinställningar';
@override
String get settings_contactSettingsSubtitle =>
'Inställningar för hur kontakter läggs till.';
@override
String get settings_privacyMode => 'Privatläge';
@@ -248,9 +395,57 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privatläge är avstängt';
@override
String get settings_privacy => 'Inställningar för sekretess';
@override
String get settings_privacySubtitle =>
'Kontrollera vilken information som delas.';
@override
String get settings_privacySettingsDescription =>
'Välj vilken information din enhet delar med andra.';
@override
String get settings_denyAll => 'Neka alla';
@override
String get settings_allowByContact => 'Tillåt via kontaktflaggor';
@override
String get settings_allowAll => 'Tillåt alla';
@override
String get settings_telemetryBaseMode => 'Telemetribasläge';
@override
String get settings_telemetryLocationMode => 'Telemetritillstånd för plats';
@override
String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge';
@override
String get settings_advertLocation => 'Annonsplacering';
@override
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
@override
String get settings_multiAck => 'Flera bekräftelser';
@override
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
@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';
@@ -290,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';
@@ -538,6 +734,48 @@ class AppLocalizationsSv extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatisk ruttrotation är avstängd';
@override
String get appSettings_maxRouteWeight => 'Maximalt tillåtet vikt för rutten';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.';
@override
String get appSettings_initialRouteWeight => 'Initial vikt för rutt';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Initial vikt för nyligen upptäckta vägar';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Ökning av vikt för framgång';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Vikt läggs till en väg efter en lyckad leverans.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Minskning av vikten för misslyckande';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Vikt som tagits bort från en väg efter ett misslyckat leveransförsök';
@override
String get appSettings_maxMessageRetries => 'Maximalt antal försök';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batteri';
@@ -744,6 +982,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
@override
String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat';
@override
String contacts_groupAlreadyExists(String name) {
return 'Gruppen \"$name\" finns redan.';
@@ -783,6 +1024,40 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Senast synlig $days dagar sedan';
}
@override
String get contact_info => 'Kontaktinformation';
@override
String get contact_settings => 'Kontaktinställningar';
@override
String get contact_telemetry => 'Telemetri';
@override
String get contact_lastSeen => 'Senast sedd';
@override
String get contact_clearChat => 'Rensa Chatt';
@override
String get contact_teleBase => 'Telemetribas';
@override
String get contact_teleBaseSubtitle =>
'Tillåt delning av batterinivå och grundläggande telemetri';
@override
String get contact_teleLoc => 'Telemetridata plats';
@override
String get contact_teleLocSubtitle => 'Tillåt delning av platsdata';
@override
String get contact_teleEnv => 'Telemetri Miljö';
@override
String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata';
@override
String get channels_title => 'Kanaler';
@@ -809,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';
@@ -951,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';
@@ -972,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...';
@@ -1168,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';
@@ -1266,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?';
@@ -1347,9 +1639,30 @@ 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';
@override
String get map_setAsMyLocation => 'Ange som min plats';
@override
String get map_pinLabel => 'Fästetikett';
@@ -1395,6 +1708,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_otherNodes => 'Andra noder';
@override
String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
@override
String get map_keyPrefix => 'Nyckelprefix';
@@ -1410,12 +1726,25 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Visa delade markörer';
@override
String get map_showGuessedLocations =>
'Visa upp de antagna nodernas placeringar';
@override
String get map_showDiscoveryContacts => 'Visa Discovery-kontakter';
@override
String get map_guessedLocation => 'Gissad plats';
@override
String get map_lastSeenTime => 'Senaste Visats Tid';
@override
String get map_sharedPin => 'Delad PIN';
@override
String get map_sharedAt => 'Delad';
@override
String get map_joinRoom => 'Gå med i rum';
@@ -1428,6 +1757,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_runTrace => 'Kör spårsökning';
@override
String get map_runTraceWithReturnPath => 'Gå tillbaka på samma väg';
@override
String get map_removeLast => 'Ta bort sista';
@@ -1702,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';
@@ -1737,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';
@@ -2109,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';
@@ -2888,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';
@@ -3081,4 +3467,310 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Senast sedd';
@override
String get contactsSettings_title => 'Kontaktinställningar';
@override
String get contactsSettings_autoAddTitle => 'Automatisk upptäckt';
@override
String get contactsSettings_otherTitle =>
'Andra inställningar relaterade till kontakt';
@override
String get contactsSettings_autoAddUsersTitle =>
'Lägg till användare automatiskt';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta användare';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Lägg till upprepande enheter automatiskt';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Lägg automatiskt till rumsservrar';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Lägg till sensorer automatiskt';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.';
@override
String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.';
@override
String get discoveredContacts_Title => 'Upptäckta kontakter';
@override
String get discoveredContacts_noMatching => 'Inga matchande kontakter';
@override
String get discoveredContacts_searchHint => 'Sök uppfunna kontakter';
@override
String get discoveredContacts_contactAdded => 'Kontakt tillagd';
@override
String get discoveredContacts_addContact => 'Lägg till kontakt';
@override
String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp';
@override
String get discoveredContacts_deleteContact => 'Ta bort kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Ta bort alla upptäckta kontakter';
@override
String get discoveredContacts_deleteContactAllContent =>
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
@override
String get chat_sendCooldown =>
'Vänligen vänta en stund innan du skickar igen.';
@override
String get appSettings_jumpToOldestUnread =>
'Gå direkt till det äldsta, obesvarade meddelandet';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.';
@override
String get appSettings_languageHu => 'Ungerskt';
@override
String get appSettings_languageJa => 'Japanska';
@override
String get appSettings_languageKo => 'Koreanska';
@override
String get radioStats_tooltip => 'Radio- och mesh-statistik';
@override
String get radioStats_screenTitle => 'Radiostation';
@override
String get radioStats_notConnected =>
'Anslut till en enhet för att visa radiostatistik.';
@override
String get radioStats_firmwareTooOld =>
'Radio statistik kräver kompatibel firmware version 8 eller senare.';
@override
String get radioStats_waiting => 'Väntar på data…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Bakgrundsnivå: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Senaste RSSI-värde: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Senaste SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX-tid (total): $seconds sekunder';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX-tid (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ljudnivå (dBm) baserat på de senaste mätningarna.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Bakgrundsnivå: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Hämtar radiostatistik…';
@override
String get radioStats_settingsTile => 'Radiostation';
@override
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';
@override
String get scanner_linuxPairingHidePin => 'Dölj PIN';
@override
String get scanner_linuxPairingPinTitle => 'BluetoothparningsPIN';
@override
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';
}
File diff suppressed because it is too large Load Diff
+651 -7
View File
@@ -38,9 +38,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get common_delete => '删除';
@override
String get common_deleteAll => '删除全部';
@override
String get common_close => '关闭';
@override
String get common_done => 'Done';
@override
String get common_edit => '编辑';
@@ -108,6 +114,123 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanner_title => '连接设备';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => '蓝牙';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => '通过 TCP 连接';
@override
String get tcpHostLabel => 'IP地址';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => '端口';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => '输入目标地址,然后连接';
@override
String tcpStatus_connectingTo(String endpoint) {
return '连接到 $endpoint...';
}
@override
String get tcpErrorHostRequired => '需要提供IP地址。';
@override
String get tcpErrorPortInvalid => '端口号必须在 1 到 65535 之间。';
@override
String get tcpErrorUnsupported => '此平台不支持 TCP 传输。';
@override
String get tcpErrorTimedOut => 'TCP 连接超时。';
@override
String tcpConnectionFailed(String error) {
return 'TCP 连接失败:$error';
}
@override
String get usbScreenTitle => '通过USB连接';
@override
String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。';
@override
String get usbScreenStatus => '选择一个 USB 设备';
@override
String get usbScreenNote => 'USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。';
@override
String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
@override
String get usbErrorPermissionDenied => '拒绝了USB权限。';
@override
String get usbErrorDeviceMissing => '所选的USB设备已不再可用。';
@override
String get usbErrorInvalidPort => '选择一个有效的USB设备。';
@override
String get usbErrorBusy => '还有一个 USB 连接请求正在进行中。';
@override
String get usbErrorNotConnected => '没有连接任何USB设备。';
@override
String get usbErrorOpenFailed => '未能打开所选的USB设备。';
@override
String get usbErrorConnectFailed => '未能连接到所选的USB设备。';
@override
String get usbErrorUnsupported => '此平台不支持USB串行通信。';
@override
String get usbErrorAlreadyActive => 'USB 连接已建立。';
@override
String get usbErrorNoDeviceSelected => '未选择任何 USB 设备。';
@override
String get usbErrorPortClosed => 'USB 连接未建立。';
@override
String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。';
@override
String get usbFallbackDeviceName => 'Web 串流设备';
@override
String get usbStatus_notConnected => '选择一个 USB 设备';
@override
String get usbStatus_connecting => '连接USB设备...';
@override
String get usbStatus_searching => '正在搜索 USB 设备...';
@override
String usbConnectionFailed(String error) {
return 'USB 连接失败:$error';
}
@override
String get scanner_scanning => '正在搜索设备...';
@@ -148,6 +271,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备';
@override
String get scanner_chromeRequired => '需要 Chrome 浏览器';
@override
String get scanner_chromeRequiredMessage =>
'此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。';
@override
String get scanner_enableBluetooth => '启用蓝牙';
@@ -226,6 +356,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_longitude => '经度';
@override
String get settings_contactSettings => '联系人设置';
@override
String get settings_contactSettingsSubtitle => '添加联系人的设置';
@override
String get settings_privacyMode => '隐私模式';
@@ -241,9 +377,55 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_privacyModeDisabled => '隐私模式已关闭';
@override
String get settings_privacy => '隐私设置';
@override
String get settings_privacySubtitle => '控制要共享的信息。';
@override
String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。';
@override
String get settings_denyAll => '拒绝所有';
@override
String get settings_allowByContact => '按联系人标志允许';
@override
String get settings_allowAll => '允许全部';
@override
String get settings_telemetryBaseMode => '遥测基础模式';
@override
String get settings_telemetryLocationMode => '遥测位置模式';
@override
String get settings_telemetryEnvironmentMode => '遥测环境模式';
@override
String get settings_advertLocation => '广告位置';
@override
String get settings_advertLocationSubtitle => '在广告中包含位置';
@override
String get settings_multiAck => '多重ACK';
@override
String get settings_telemetryModeUpdated => '遥测模式已更新';
@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 => '发送广播';
@@ -281,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 => '应用调试日志';
@@ -515,6 +698,43 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用';
@override
String get appSettings_maxRouteWeight => '最大路径重量';
@override
String get appSettings_maxRouteWeightSubtitle => '一条路径可以累积的最大重量,取决于成功交付的数量。';
@override
String get appSettings_initialRouteWeight => '初始路线权重';
@override
String get appSettings_initialRouteWeightSubtitle => '新发现路径的初始重量';
@override
String get appSettings_routeWeightSuccessIncrement => '成功权重增加';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'在成功交付后,将重量添加到路径中';
@override
String get appSettings_routeWeightFailureDecrement => '失败权重降低';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'从一条路径上移除的货物,由于无法成功交付而移除。';
@override
String get appSettings_maxMessageRetries => '最大消息重试次数';
@override
String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => '电池';
@@ -712,6 +932,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_groupNameRequired => '请输入群聊名称';
@override
String get contacts_groupNameReserved => '该群组名称已被保留';
@override
String contacts_groupAlreadyExists(String name) {
return '名为 \"$name\" 的群聊已存在';
@@ -750,6 +973,39 @@ class AppLocalizationsZh extends AppLocalizations {
return '最后在线 $days 天前';
}
@override
String get contact_info => '联系信息';
@override
String get contact_settings => '联系人设置';
@override
String get contact_telemetry => '遥测数据';
@override
String get contact_lastSeen => '最近出现';
@override
String get contact_clearChat => '清除聊天记录';
@override
String get contact_teleBase => '遥测基站';
@override
String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据';
@override
String get contact_teleLoc => '遥测位置';
@override
String get contact_teleLocSubtitle => '允许共享位置数据';
@override
String get contact_teleEnv => '遥测环境';
@override
String get contact_teleEnvSubtitle => '允许共享环境传感器数据';
@override
String get channels_title => '频道';
@@ -776,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 => '私有';
@@ -914,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 => '发送消息开始对话';
@@ -933,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 => '输入消息...';
@@ -1120,6 +1384,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get chat_successes => '成功';
@override
String get chat_score => 'Score';
@override
String get chat_removePath => '移除路径';
@@ -1207,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 => '打开链接?';
@@ -1285,9 +1558,30 @@ 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 => '在此分享标记';
@override
String get map_setAsMyLocation => '设置为我的位置';
@override
String get map_pinLabel => '标签';
@@ -1332,6 +1626,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_otherNodes => '其他节点';
@override
String get map_showOverlaps => '重复键重叠';
@override
String get map_keyPrefix => '关键字前缀';
@@ -1347,12 +1644,24 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_showSharedMarkers => '显示共享标记';
@override
String get map_showGuessedLocations => '显示猜测的节点位置';
@override
String get map_showDiscoveryContacts => '显示发现联系人';
@override
String get map_guessedLocation => '猜测的位置';
@override
String get map_lastSeenTime => '最后在线时间';
@override
String get map_sharedPin => '共享标记';
@override
String get map_sharedAt => '已分享';
@override
String get map_joinRoom => '加入房间';
@@ -1365,6 +1674,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_runTrace => '运行路径追踪';
@override
String get map_runTraceWithReturnPath => '沿着相同的路径返回';
@override
String get map_removeLast => '移除最后一个';
@@ -1622,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 => '状态';
@@ -1655,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 => '转发节点状态';
@@ -2012,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 => '发送广播包';
@@ -2727,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 => '频率';
@@ -2890,4 +3253,285 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get snrIndicator_lastSeen => '最近访问';
@override
String get contactsSettings_title => '联系人设置';
@override
String get contactsSettings_autoAddTitle => '自动发现';
@override
String get contactsSettings_otherTitle => '其他联系人相关设置';
@override
String get contactsSettings_autoAddUsersTitle => '自动添加用户';
@override
String get contactsSettings_autoAddUsersSubtitle => '允许伴侣自动添加发现的用户';
@override
String get contactsSettings_autoAddRepeatersTitle => '自动添加重复器';
@override
String get contactsSettings_autoAddRepeatersSubtitle => '允许伴侣自动添加发现的重复器';
@override
String get contactsSettings_autoAddRoomServersTitle => '自动添加房间服务器';
@override
String get contactsSettings_autoAddRoomServersSubtitle => '允许伴侣自动添加发现的房间服务器';
@override
String get contactsSettings_autoAddSensorsTitle => '自动添加传感器';
@override
String get contactsSettings_autoAddSensorsSubtitle => '允许伴侣自动添加发现的传感器';
@override
String get contactsSettings_overwriteOldestTitle => '覆盖最旧的';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'当联系人列表已满时,将替换最老的非收藏联系人。';
@override
String get discoveredContacts_Title => '已发现的联系人';
@override
String get discoveredContacts_noMatching => '没有匹配的联系人';
@override
String get discoveredContacts_searchHint => '搜索已发现的联系人';
@override
String get discoveredContacts_contactAdded => '联系人已添加';
@override
String get discoveredContacts_addContact => '添加联系人';
@override
String get discoveredContacts_copyContact => '复制联系人到剪贴板';
@override
String get discoveredContacts_deleteContact => '删除联系人';
@override
String get discoveredContacts_deleteContactAll => '删除所有发现的联系人';
@override
String get discoveredContacts_deleteContactAllContent => '您确定要删除所有发现的联系人吗?';
@override
String get chat_sendCooldown => '请稍等片刻后再尝试发送。';
@override
String get appSettings_jumpToOldestUnread => '跳转到最旧、未读的文章';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。';
@override
String get appSettings_languageHu => '匈牙利';
@override
String get appSettings_languageJa => '日语';
@override
String get appSettings_languageKo => '韩语';
@override
String get radioStats_tooltip => '无线电和网状结构统计数据';
@override
String get radioStats_screenTitle => '广播统计数据';
@override
String get radioStats_notConnected => '连接到设备以查看收音机统计信息。';
@override
String get radioStats_firmwareTooOld => '使用无线电统计功能需要配合使用 v8 或更高版本的固件。';
@override
String get radioStats_waiting => '正在等待数据…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return '噪声水平:$noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return '上次 RSSI 值:$rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return '上次 SNR$snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX 频道播出时间(总时长):$seconds';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX 使用时长(总时长):$seconds';
}
@override
String get radioStats_chartCaption => '近期的噪声水平(dBm)。';
@override
String radioStats_stripNoise(int noiseDbm) {
return '噪声水平:$noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => '正在获取收音机数据…';
@override
String get radioStats_settingsTile => '广播统计数据';
@override
String get radioStats_settingsSubtitle => '噪声水平、RSSI、信噪比和空中时间';
@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码';
@override
String get scanner_linuxPairingHidePin => '隐藏 PIN';
@override
String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN';
@override
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';
}
+397 -47
View File
@@ -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": {
@@ -285,6 +287,7 @@
"contacts_newGroup": "Nieuwe Groep",
"contacts_groupName": "Groepnaam",
"contacts_groupNameRequired": "De groepnaam is verplicht.",
"contacts_groupNameReserved": "Deze groepsnaam is gereserveerd",
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -681,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",
@@ -877,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",
@@ -911,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",
@@ -981,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",
@@ -997,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",
@@ -1007,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": {
@@ -1072,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": {
@@ -1093,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",
@@ -1109,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.",
@@ -1202,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",
@@ -1345,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",
@@ -1362,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",
@@ -1573,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",
@@ -1605,12 +1608,14 @@
"map_runTrace": "Padeshulp traceren",
"scanner_enableBluetooth": "Activeer Bluetooth",
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
"scanner_chromeRequired": "Chrome-browser vereist",
"scanner_chromeRequiredMessage": "Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.",
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
"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",
@@ -1799,5 +1804,350 @@
"contacts_searchContactsNoNumber": "Zoek contacten...",
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
"contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen",
"contactsSettings_title": "Instellingen voor contacten",
"settings_contactSettings": "Contactinstellingen",
"contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken",
"contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt",
"contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen",
"contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.",
"contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen",
"settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten",
"contactsSettings_autoAddTitle": "Automatische detectie",
"contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt",
"contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt",
"contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen",
"contactsSettings_overwriteOldestTitle": "Overschrijf Oudste",
"discoveredContacts_noMatching": "Geen overeenkomende contacten",
"discoveredContacts_addContact": "Contact toevoegen",
"discoveredContacts_copyContact": "Kopieer contact naar klembord",
"discoveredContacts_deleteContact": "Contact verwijderen",
"discoveredContacts_Title": "Ontdekte contacten",
"discoveredContacts_contactAdded": "Contact toegevoegd",
"discoveredContacts_searchHint": "Ontdekte contacten zoeken",
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
"common_deleteAll": "Alles verwijderen",
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
"map_guessedLocation": "Geroerde locatie",
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Verbind via USB",
"usbScreenStatus": "Selecteer een USB-apparaat",
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.",
"usbErrorPermissionDenied": "Toegang via USB is geweigerd.",
"usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.",
"usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.",
"usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.",
"usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.",
"usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.",
"usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.",
"usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.",
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
"usbFallbackDeviceName": "Web-serieapparaat",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbConnectionFailed": "Fout bij de USB-verbinding: {error}",
"usbStatus_notConnected": "Selecteer een USB-apparaat",
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
"usbStatus_searching": "Zoeken naar USB-apparaten...",
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpScreenTitle": "Verbind via TCP",
"tcpHostLabel": "IP-adres",
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Poort",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Voer het eindpunt in en verbind",
"tcpStatus_connectingTo": "Verbinding maken met {endpoint}...",
"tcpErrorHostRequired": "Een IP-adres is vereist.",
"tcpErrorPortInvalid": "De poortwaarde moet tussen 1 en 65535 liggen.",
"tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.",
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
"map_showDiscoveryContacts": "Ontdek contacten weergeven",
"map_setAsMyLocation": "Stel dit in als mijn locatie",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Privacyinstellingen",
"settings_privacySubtitle": "Beheer welke informatie wordt gedeeld",
"settings_telemetryLocationMode": "Telemetrie-locatiemodus",
"settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus",
"settings_advertLocation": "Advertentielocatie",
"settings_advertLocationSubtitle": "Locatie opnemen in advertentie",
"settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen",
"settings_allowByContact": "Toestaan op basis van contactvlaggen",
"settings_allowAll": "Alles toestaan",
"settings_denyAll": "Weiger alles",
"contact_info": "Contactinformatie",
"settings_telemetryBaseMode": "Telemetrie-basismodus",
"contact_teleBase": "Telemetrie_basis",
"contact_teleLoc": "Telemetrielocatie",
"contact_teleLocSubtitle": "Locatiegegevens delen toestaan",
"contact_teleEnv": "Telemetrieomgeving",
"contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan",
"contact_settings": "Contactinstellingen",
"contact_telemetry": "Telemetrie",
"contact_lastSeen": "Laatst gezien",
"contact_clearChat": "Chat leegmaken",
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
"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",
"appSettings_initialRouteWeightSubtitle": "Startgewicht voor nieuwe, ontdekte routes",
"appSettings_routeWeightSuccessIncrement": "Toename in het gewicht van het succes",
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht wordt toegevoegd aan een route na een succesvolle levering.",
"appSettings_routeWeightFailureDecrement": "Vermindering van het gewicht van fouten",
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering",
"appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen",
"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",
"map_showOverlaps": "Herhalingssleutel overlapt",
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Ga naar het oudste ongelezen bericht",
"appSettings_jumpToOldestUnreadSubtitle": "Bij het openen van een chat met ongelezen berichten, scroll dan naar het eerste ongelezen bericht, in plaats van naar het meest recente.",
"chat_sendCooldown": "Gelieve even te wachten voordat u opnieuw verzendt.",
"appSettings_languageHu": "Hongaars",
"appSettings_languageJa": "Japanisch",
"appSettings_languageKo": "Koreaans",
"radioStats_tooltip": "Statistieken voor radio en mesh-netwerken",
"radioStats_screenTitle": "Statistieken over radio",
"radioStats_notConnected": "Verbind met een apparaat om radio-statistieken te bekijken.",
"radioStats_firmwareTooOld": "Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.",
"radioStats_waiting": "Wacht op gegevens…",
"radioStats_noiseFloor": "Ruisfrequentie: {noiseDbm} dBm",
"radioStats_lastRssi": "Laatste RSSI-waarde: {rssiDbm} dBm",
"radioStats_lastSnr": "Laatste SNR: {snr} dB",
"radioStats_txAir": "TX-tijd (totaal): {seconds} s",
"radioStats_rxAir": "Tijd besteed met RX (totaal): {seconds} s",
"radioStats_chartCaption": "Ruisfrequentie (dBm) over recente metingen.",
"radioStats_stripNoise": "Ruisfrequentie: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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": "BluetoothkoppelingsPIN",
"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})."
}
+604 -216
View File
File diff suppressed because it is too large Load Diff
+351 -1
View File
@@ -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",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Novo Grupo",
"contacts_groupName": "Nome do grupo",
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
"contacts_groupNameReserved": "Este nome de grupo está reservado",
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1606,6 +1609,8 @@
"scanner_enableBluetooth": "Ative o Bluetooth",
"scanner_bluetoothOff": "Bluetooth está desativado",
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
"scanner_chromeRequired": "Navegador Chrome necessário",
"scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.",
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
"snrIndicator_lastSeen": "Visto pela última vez",
"chat_ShowAllPaths": "Mostrar todos os caminhos",
@@ -1799,5 +1804,350 @@
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
"contacts_unread": "Não lido",
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
"settings_contactSettings": "Configurações de Contato",
"contactsSettings_otherTitle": "Outras configurações relacionadas a contatos",
"contactsSettings_title": "Configurações de contatos",
"contactsSettings_autoAddTitle": "Descoberta Automática",
"settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados",
"contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente",
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.",
"contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala",
"contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo",
"contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente",
"discoveredContacts_Title": "Contatos Descobertos",
"contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.",
"contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente",
"discoveredContacts_noMatching": "Nenhum contato correspondente",
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.",
"discoveredContacts_searchHint": "Pesquisar contatos descobertos",
"contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.",
"discoveredContacts_copyContact": "Copiar Contato para a área de transferência",
"discoveredContacts_deleteContact": "Excluir Contato",
"discoveredContacts_contactAdded": "Contato adicionado",
"discoveredContacts_addContact": "Adicionar Contato",
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
"common_deleteAll": "Excluir Tudo",
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
"map_guessedLocation": "Localização estimada",
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Conecte via USB",
"usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.",
"usbScreenStatus": "Selecione um dispositivo USB",
"usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.",
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.",
"usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.",
"usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.",
"usbErrorInvalidPort": "Selecione um dispositivo USB válido.",
"usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.",
"usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.",
"usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.",
"usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.",
"usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.",
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
"usbErrorPortClosed": "A conexão USB não está ativa.",
"usbFallbackDeviceName": "Dispositivo de Serial para a Web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Procurando por dispositivos USB...",
"usbStatus_notConnected": "Selecione um dispositivo USB",
"usbConnectionFailed": "Falha na conexão USB: {error}",
"usbStatus_connecting": "Conectando ao dispositivo USB...",
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Endereço IP",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Estabelecer conexão via TCP",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Porta",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Insira o endereço final e conecte-se.",
"tcpStatus_connectingTo": "Conectando a {endpoint}...",
"tcpErrorHostRequired": "É necessário fornecer um endereço IP.",
"tcpErrorPortInvalid": "O valor do porto deve estar entre 1 e 65535.",
"tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.",
"tcpErrorTimedOut": "A conexão TCP expirou.",
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
"map_setAsMyLocation": "Defina minha localização",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.",
"settings_allowByContact": "Permitir por bandeiras de contato",
"settings_telemetryLocationMode": "Modo de Localização de Telemetria",
"settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria",
"settings_advertLocation": "Localização do Anúncio",
"settings_advertLocationSubtitle": "Incluir localização no anúncio",
"settings_privacySubtitle": "Controle o que é compartilhado.",
"settings_denyAll": "Negar todos",
"settings_allowAll": "Permitir todos",
"settings_privacy": "Configurações de Privacidade",
"contact_info": "Informações de Contato",
"settings_telemetryBaseMode": "Modo Base de Telemetria",
"contact_teleBase": "Base de Telemetria",
"contact_teleLoc": "Localização de Telemetria",
"contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização",
"contact_teleEnv": "Ambiente de Telemetria",
"contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente",
"contact_lastSeen": "Visto pela última vez",
"contact_clearChat": "Limpar Chat",
"contact_telemetry": "Telemetria",
"contact_settings": "Configurações de Contato",
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
"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.",
"appSettings_initialRouteWeightSubtitle": "Peso inicial para novos caminhos descobertos",
"appSettings_routeWeightSuccessIncrement": "Aumento do peso para indicar sucesso",
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso adicionado a um caminho após a entrega bem-sucedida.",
"appSettings_routeWeightFailureDecrement": "Redução do peso da falha",
"appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.",
"appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens",
"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",
"map_showOverlaps": "Sobreposições da Chave Repeater",
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Vá para a mensagem mais antiga não lida",
"chat_sendCooldown": "Por favor, aguarde um momento antes de reenviar.",
"appSettings_languageHu": "Húngaro",
"appSettings_jumpToOldestUnreadSubtitle": "Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.",
"appSettings_languageJa": "Japonês",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Estatísticas de rádio e malha",
"radioStats_screenTitle": "Estatísticas de rádio",
"radioStats_notConnected": "Conecte-se a um dispositivo para visualizar estatísticas de rádio.",
"radioStats_firmwareTooOld": "As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.",
"radioStats_waiting": "Aguardando dados…",
"radioStats_noiseFloor": "Nível de ruído: {noiseDbm} dBm",
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Último SNR: {snr} dB",
"radioStats_txAir": "Tempo de transmissão da TX (total): {seconds} s",
"radioStats_rxAir": "Tempo de uso do RX (total): {seconds} s",
"radioStats_chartCaption": "Nível de ruído (dBm) em amostras recentes.",
"radioStats_stripNoise": "Nível de ruído: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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",
"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})."
}
+351 -1
View File
@@ -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": "Анонсирование отправлено",
@@ -212,6 +214,7 @@
"contacts_newGroup": "Новая группа",
"contacts_groupName": "Имя группы",
"contacts_groupNameRequired": "Имя группы обязательно",
"contacts_groupNameReserved": "Это имя группы зарезервировано",
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
"contacts_filterContacts": "Фильтр контактов...",
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
@@ -846,6 +849,8 @@
"scanner_enableBluetooth": "Включите Bluetooth",
"scanner_bluetoothOff": "Bluetooth выключен",
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
"scanner_chromeRequired": "Требуется браузер Chrome",
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
"snrIndicator_lastSeen": "Последний раз видели",
"chat_ShowAllPaths": "Показать все пути",
@@ -1039,5 +1044,350 @@
"contacts_unread": "Непрочитанное",
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
"contacts_searchUsers": "Поиск {number}{str} пользователей...",
"settings_contactSettings": "Настройки контактов",
"settings_contactSettingsSubtitle": "Настройки добавления контактов",
"contactsSettings_autoAddTitle": "Автоматическое обнаружение",
"contactsSettings_title": "Настройки контактов",
"contactsSettings_otherTitle": "Другие настройки, связанные с контактами",
"contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей",
"contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат",
"contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики",
"contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики",
"contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей",
"contactsSettings_overwriteOldestTitle": "Перезаписать самое старое",
"contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы",
"contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы",
"contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.",
"discoveredContacts_noMatching": "Нет совпадающих контактов",
"discoveredContacts_searchHint": "Найденные контакты поиска",
"discoveredContacts_contactAdded": "Контакт добавлен",
"discoveredContacts_copyContact": "Копировать контакт в буфер обмена",
"discoveredContacts_addContact": "Добавить контакт",
"discoveredContacts_Title": "Обнаруженные контакты",
"discoveredContacts_deleteContact": "Удалить контакт",
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
"common_deleteAll": "Удалить все",
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
"map_guessedLocation": "Угаданное место",
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.",
"usbScreenTitle": "Подключение через USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Выберите USB-устройство",
"usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.",
"usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.",
"usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.",
"usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.",
"usbErrorInvalidPort": "Выберите действительное USB-устройство.",
"usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.",
"usbErrorNotConnected": "Ни одно USB-устройство не подключено.",
"usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.",
"usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.",
"usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.",
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
"usbErrorPortClosed": "USB-соединение не установлено.",
"usbFallbackDeviceName": "Устройство для последовательного подключения к сети",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Поиск USB-устройств...",
"usbStatus_connecting": "Подключение к USB-устройству...",
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
"usbStatus_notConnected": "Выберите USB-устройство",
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP-адрес",
"tcpScreenTitle": "Установить соединение по протоколу TCP",
"tcpPortLabel": "Порт",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Введите адрес и подключитесь.",
"tcpStatus_connectingTo": "Подключение к {endpoint}...",
"tcpErrorHostRequired": "Необходимо указать IP-адрес.",
"tcpErrorPortInvalid": "Порт должен находиться в диапазоне от 1 до 65535.",
"tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.",
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
"map_showDiscoveryContacts": "Показать контакты Discovery",
"map_setAsMyLocation": "Установить мое местоположение",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Настройки конфиденциальности",
"settings_privacySubtitle": "Контролируйте, какую информацию делиться.",
"settings_telemetryLocationMode": "Режим местоположения телеметрии",
"settings_telemetryEnvironmentMode": "Режим среды телеметрии",
"settings_advertLocation": "Местоположение рекламы",
"settings_advertLocationSubtitle": "Включить местоположение в объявление",
"settings_allowAll": "Разрешить все",
"settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.",
"settings_denyAll": "Отклонить все",
"settings_allowByContact": "Разрешить по флагам контактов",
"contact_info": "Контактная информация",
"settings_telemetryBaseMode": "Базовый режим телеметрии",
"contact_teleBase": "База телеметрии",
"contact_teleLoc": "Местоположение телеметрии",
"contact_teleLocSubtitle": "Разрешить обмен данными о местоположении",
"contact_teleEnv": "Среда телеметрии",
"contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды",
"contact_settings": "Настройки контактов",
"contact_telemetry": "Телеметрия",
"contact_clearChat": "Очистить чат",
"contact_lastSeen": "Последний раз видели",
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
"appSettings_initialRouteWeight": "Начальный вес маршрута",
"appSettings_routeWeightSuccessIncrement": "Увеличение веса успеха",
"appSettings_routeWeightSuccessIncrementSubtitle": "Вес, добавленный к маршруту после успешной доставки.",
"appSettings_routeWeightFailureDecrement": "Уменьшение веса неудачи",
"appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.",
"appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения",
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"map_showOverlaps": "Перекрытия ключа повтора",
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.",
"appSettings_jumpToOldestUnread": "Перейти к самому старому непрочитанному сообщению",
"appSettings_languageHu": "Венгерский",
"appSettings_jumpToOldestUnreadSubtitle": "При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.",
"appSettings_languageJa": "Японский",
"appSettings_languageKo": "Корейский",
"radioStats_tooltip": "Статистика радио и беспроводной сети",
"radioStats_screenTitle": "Статистика радиовещания",
"radioStats_notConnected": "Подключитесь к устройству, чтобы просмотреть статистику радио.",
"radioStats_firmwareTooOld": "Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.",
"radioStats_waiting": "Ожидаем данных…",
"radioStats_noiseFloor": "Уровень шума: {noiseDbm} дБм",
"radioStats_lastRssi": "Последнее значение RSSI: {rssiDbm} дБм",
"radioStats_lastSnr": "Последнее значение SNR: {snr} дБ",
"radioStats_txAir": "Время эфира на телеканале TX (общее): {seconds} секунд",
"radioStats_rxAir": "Общее время использования RX (в секундах): {seconds} с",
"radioStats_chartCaption": "Уровень шума (дБм) на основе последних измерений.",
"radioStats_stripNoise": "Уровень шума: {noiseDbm} дБм",
"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": {
"type": "String"
}
}
},
"@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",
"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})."
}
+351 -1
View File
@@ -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á",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Nová skupina",
"contacts_groupName": "Názov skupiny",
"contacts_groupNameRequired": "Skupina musí mať názov.",
"contacts_groupNameReserved": "Tento názov skupiny je rezervovaný",
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1604,6 +1607,8 @@
"map_runTrace": "Spustiť trasovaním cesty",
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
"scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome",
"scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.",
"scanner_bluetoothOff": "Bluetooth je vypnutý",
"scanner_enableBluetooth": "Povolte Bluetooth",
"snrIndicator_lastSeen": "Naposledy videný",
@@ -1799,5 +1804,350 @@
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
"contacts_unread": "Neprečítané"
"contacts_unread": "Neprečítané",
"settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.",
"contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov",
"contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.",
"contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače",
"contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti",
"contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.",
"contactsSettings_autoAddTitle": "Automatické zisťovanie",
"contactsSettings_title": "Nastavenia kontaktov",
"contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami",
"settings_contactSettings": "Nastavenia kontaktov",
"contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory",
"discoveredContacts_noMatching": "Žiadne zhodné kontakty",
"discoveredContacts_searchHint": "Vyhľadať objavené kontakty",
"contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.",
"discoveredContacts_contactAdded": "Kontakt bol pridaný",
"discoveredContacts_copyContact": "Kopírovať kontakt do schránky",
"discoveredContacts_deleteContact": "Zmazať kontakt",
"contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.",
"discoveredContacts_Title": "Objavené kontakty",
"contactsSettings_overwriteOldestTitle": "Prepísať najstaršie",
"discoveredContacts_addContact": "Pridať kontakt",
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
"common_deleteAll": "Zmazať všetko",
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
"map_guessedLocation": "Odhadnutá lokalita",
"usbScreenTitle": "Pripojte cez USB",
"usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Vyberte USB zariadenie",
"usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.",
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.",
"usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.",
"usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.",
"usbErrorInvalidPort": "Vyberte platné USB zariadenie.",
"usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.",
"usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.",
"usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.",
"usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.",
"usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.",
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
"usbFallbackDeviceName": "Webový sériový zariadenie",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Hľadanie USB zariadení...",
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
"usbStatus_notConnected": "Vyberte USB zariadenie",
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP adresa",
"tcpScreenTitle": "Spojte sa pomocou protokolu TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.",
"tcpStatus_connectingTo": "Pripojenie k {endpoint}...",
"tcpErrorHostRequired": "Je potrebné zadať IP adresu.",
"tcpErrorPortInvalid": "Číslo portu musí byť medzi 1 a 65535.",
"tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.",
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
"map_showDiscoveryContacts": "Zobraziť kontakty objavov",
"map_setAsMyLocation": "Nastavte ako moju polohu",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Nastavenia súkromia",
"settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.",
"settings_telemetryLocationMode": "Režim umiestnenia telemetrie",
"settings_telemetryBaseMode": "Základný režim telemetrie",
"settings_advertLocation": "Umiestnenie inzerátu",
"settings_telemetryEnvironmentMode": "Režim prostredia telemetrie",
"settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu",
"settings_allowAll": "Povoliť všetko",
"settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.",
"settings_denyAll": "Zamietnuť všetko",
"settings_allowByContact": "Povoliť podľa kontaktových vlajok",
"contact_info": "Kontaktné informácie",
"contact_settings": "Nastavenia kontaktov",
"contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie",
"contact_teleLoc": "Lokácia telemetrie",
"contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite",
"contact_teleEnv": "Prostredie telemetrie",
"contact_telemetry": "Telemetria",
"contact_clearChat": "Vymazať chat",
"contact_lastSeen": "Naposledy videný",
"contact_teleBase": "Báza telemetrie",
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
"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",
"appSettings_maxRouteWeight": "Maximálna hmotnosť trasy",
"appSettings_routeWeightSuccessIncrement": "Zvyšenie váhy úspechu",
"appSettings_routeWeightSuccessIncrementSubtitle": "Hmotnosť pridaná k trase po úspešnej doručení",
"appSettings_routeWeightFailureDecrement": "Sníženie váhy, ktorá sa používa na odhad rizika.",
"appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie",
"appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ",
"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",
"map_showOverlaps": "Prekrývanie opakovača kľúča",
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Prosím, počkajte chvíľu, než zašlete znova.",
"appSettings_jumpToOldestUnread": "Presk oceň",
"appSettings_jumpToOldestUnreadSubtitle": "Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.",
"appSettings_languageHu": "Maďarský",
"appSettings_languageJa": "Japonský",
"appSettings_languageKo": "Kórejský",
"radioStats_tooltip": "Statistiky rádiových a sieťových kanálov",
"radioStats_screenTitle": "Štatistiky rádiových vysielaní",
"radioStats_notConnected": "Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.",
"radioStats_firmwareTooOld": "Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.",
"radioStats_waiting": "Čakám na údaje…",
"radioStats_noiseFloor": "Úroveň hluku: {noiseDbm} dBm",
"radioStats_lastRssi": "Posledný údaj RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Posledná hodnota SNR: {snr} dB",
"radioStats_txAir": "Čas vysielania na TX (celkový): {seconds} s",
"radioStats_rxAir": "Čas RX (celkový): {seconds} s",
"radioStats_chartCaption": "Úroveň šumu (dBm) pre posledné vzorky.",
"radioStats_stripNoise": "Úroveň hluku: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"scanner_linuxPairingHidePin": "Skryť 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})."
}
+351 -1
View File
@@ -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",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Nova skupina",
"contacts_groupName": "Ime skupine",
"contacts_groupNameRequired": "Ime skupine je obvezno.",
"contacts_groupNameReserved": "To ime skupine je rezervirano",
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1608,8 @@
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
"scanner_enableBluetooth": "Omogočite Bluetooth",
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
"scanner_chromeRequired": "Zahtevan brskalnik Chrome",
"scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.",
"scanner_bluetoothOff": "Bluetooth je izklopljen",
"snrIndicator_lastSeen": "Zadnjič videno",
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
@@ -1799,5 +1804,350 @@
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
"contacts_searchContactsNoNumber": "Iskanje stikov...",
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
"contacts_searchUsers": "Išči {number}{str} uporabnikov...",
"settings_contactSettings": "Nastavitve stika",
"contactsSettings_autoAddTitle": "Avtomatsko odkrivanje",
"contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike",
"contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje",
"contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.",
"contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob",
"contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.",
"contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki",
"settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.",
"contactsSettings_title": "Nastavitve stikov",
"contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje",
"contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.",
"discoveredContacts_noMatching": "Ni ujemajočih stikov",
"contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.",
"discoveredContacts_addContact": "Dodaj stik",
"discoveredContacts_contactAdded": "Kontakt dodan",
"discoveredContacts_copyContact": "Kopiraj stik v odložišče",
"contactsSettings_overwriteOldestTitle": "Prepiši najstarejše",
"discoveredContacts_Title": "Odkriti stiki",
"discoveredContacts_searchHint": "Najdeni stiki po iskanju",
"discoveredContacts_deleteContact": "Izbriši stik",
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
"common_deleteAll": "Izbriši vse",
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
"map_guessedLocation": "Predpostavljena lokacija",
"map_showGuessedLocations": "Pokaži lokacije domnevnih not.",
"usbScreenTitle": "Povežite preko USB",
"usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenStatus": "Izberite USB naprave",
"usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.",
"usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.",
"usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.",
"usbErrorDeviceMissing": "Izbrani USB napravej je več ne.",
"usbErrorInvalidPort": "Izberite veljavno USB naprave.",
"usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.",
"usbErrorNotConnected": "Ni priklopljenih USB naprave.",
"usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.",
"usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.",
"usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.",
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
"usbErrorPortClosed": "USB povezava ni aktivirana.",
"usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_notConnected": "Izberite USB naprave.",
"usbStatus_connecting": "Povezava z USB napravo...",
"usbStatus_searching": "Iskanje USB naprav...",
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP naslov",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Komunicirajte preko protokola TCP",
"tcpPortLabel": "Vrata",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Vnesite končni naslov in se povežite",
"tcpStatus_connectingTo": "Povezava z {endpoint}...",
"tcpErrorHostRequired": "Potrebna je IP-naslov.",
"tcpErrorPortInvalid": "Port mora biti med 1 in 65535.",
"tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.",
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
"map_setAsMyLocation": "Nastavite to kot mojo lokacijo",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Nastavitve zasebnosti",
"settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.",
"settings_telemetryBaseMode": "Osnovni način telemetrije",
"settings_telemetryLocationMode": "Način delovanja telemetrije",
"settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije",
"settings_advertLocation": "Lokacija oglasa",
"settings_allowByContact": "Dovoli po kontaktnih zastavah",
"settings_denyAll": "Zavrniti vse",
"settings_allowAll": "Dovoli vse",
"settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.",
"contact_info": "Kontaktni podatki",
"contact_teleBase": "Baza telemetrije",
"contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije",
"contact_teleLoc": "Lokacija telemetrije",
"contact_lastSeen": "Zadnjič videno",
"contact_settings": "Nastavitve stika",
"settings_advertLocationSubtitle": "Vključi lokacijo v oglas.",
"contact_telemetry": "Telemetrija",
"contact_clearChat": "Počisti klepet",
"contact_teleEnv": "Okolje telemetrije",
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
"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",
"appSettings_maxRouteWeight": "Največja dovoljena teža poti",
"appSettings_routeWeightSuccessIncrement": "Učinkovitost: povečanje",
"appSettings_routeWeightSuccessIncrementSubtitle": "Težava, dodana poti po uspešni dostavi",
"appSettings_routeWeightFailureDecrement": "Zmanjšanje teže, ki je povezana s pomanjkanjem",
"appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.",
"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_telemetryModeUpdated": "Način telemetrije posodobljen",
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_languageHu": "Madžarski",
"appSettings_jumpToOldestUnreadSubtitle": "Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.",
"chat_sendCooldown": "Prosimo, počakajte trenutek, preden pošljete ponovno.",
"appSettings_jumpToOldestUnread": "Pritisnite za najstarejše nepročitano sporočilo",
"appSettings_languageJa": "Japonski",
"appSettings_languageKo": "Korejski",
"radioStats_tooltip": "Statistike za radio in mrežo",
"radioStats_notConnected": "Povežite se z napravo, da si ogledate statistiko o radiju.",
"radioStats_screenTitle": "Radijske statistike",
"radioStats_firmwareTooOld": "Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.",
"radioStats_waiting": "Čakam na podatke…",
"radioStats_noiseFloor": "Število šuma: {noiseDbm} dBm",
"radioStats_lastRssi": "Najkasnejše vrednost RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Najkasnejše vrednost SNR: {snr} dB",
"radioStats_txAir": "Čas na TX (skupno): {seconds} s",
"radioStats_rxAir": "Čas, namenjen RX-ju (skupno): {seconds} s",
"radioStats_chartCaption": "Ravnovredna raven šuma (dBm) za nedavne vzorce.",
"radioStats_stripNoise": "Število šuma: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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",
"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})."
}
+351 -1
View File
@@ -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",
@@ -285,6 +287,7 @@
"contacts_newGroup": "Ny grupp",
"contacts_groupName": "Gruppnamn",
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
"contacts_groupNameReserved": "Detta gruppnamn är reserverat",
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1608,8 @@
"map_removeLast": "Ta bort sista",
"scanner_enableBluetooth": "Aktivera Bluetooth",
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
"scanner_chromeRequired": "Chrome-webbläsare krävs",
"scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.",
"scanner_bluetoothOff": "Bluetooth är avstängt",
"snrIndicator_lastSeen": "Senast sedd",
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
@@ -1799,5 +1804,350 @@
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
"contacts_searchUsers": "Sök {number}{str} användare...",
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
"settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.",
"settings_contactSettings": "Kontaktinställningar",
"contactsSettings_autoAddTitle": "Automatisk upptäckt",
"contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt",
"contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare",
"contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt",
"contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.",
"contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt",
"contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt",
"contactsSettings_title": "Kontaktinställningar",
"contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.",
"contactsSettings_overwriteOldestTitle": "Skriv över äldst",
"contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.",
"contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar",
"discoveredContacts_noMatching": "Inga matchande kontakter",
"discoveredContacts_searchHint": "Sök uppfunna kontakter",
"discoveredContacts_deleteContact": "Ta bort kontakt",
"discoveredContacts_Title": "Upptäckta kontakter",
"discoveredContacts_contactAdded": "Kontakt tillagd",
"discoveredContacts_addContact": "Lägg till kontakt",
"discoveredContacts_copyContact": "Kopiera kontakt till urklipp",
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
"common_deleteAll": "Ta bort alla",
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
"map_guessedLocation": "Gissad plats",
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
"usbScreenTitle": "Anslut via USB",
"usbScreenStatus": "Välj en USB-enhet",
"usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.",
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
"usbErrorInvalidPort": "Välj en giltig USB-enhet.",
"usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.",
"usbErrorNotConnected": "Ingen USB-enhet är ansluten.",
"usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.",
"usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.",
"usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.",
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
"usbFallbackDeviceName": "Web-serieenhet",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Anslutning till USB-enhet...",
"usbStatus_notConnected": "Välj en USB-enhet",
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
"usbStatus_searching": "Söker efter USB-enheter...",
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP-adress",
"tcpScreenTitle": "Anslut via TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Ange slutpunkt och anslut",
"tcpStatus_connectingTo": "Anslutning till {endpoint}...",
"tcpErrorHostRequired": "IP-adress krävs.",
"tcpErrorPortInvalid": "Porten måste vara mellan 1 och 65535.",
"tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.",
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
"map_showDiscoveryContacts": "Visa Discovery-kontakter",
"map_setAsMyLocation": "Ange som min plats",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Inställningar för sekretess",
"settings_allowAll": "Tillåt alla",
"settings_privacySubtitle": "Kontrollera vilken information som delas.",
"settings_telemetryEnvironmentMode": "Telemetri miljöläge",
"settings_telemetryBaseMode": "Telemetribasläge",
"settings_telemetryLocationMode": "Telemetritillstånd för plats",
"settings_advertLocation": "Annonsplacering",
"contact_info": "Kontaktinformation",
"contact_settings": "Kontaktinställningar",
"contact_telemetry": "Telemetri",
"settings_denyAll": "Neka alla",
"settings_allowByContact": "Tillåt via kontaktflaggor",
"settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.",
"contact_lastSeen": "Senast sedd",
"contact_clearChat": "Rensa Chatt",
"contact_teleEnv": "Telemetri Miljö",
"settings_advertLocationSubtitle": "Inkludera plats i annonsen",
"contact_teleEnvSubtitle": "Tillåt delning av miljösensordata",
"contact_teleBase": "Telemetribas",
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
"contact_teleLoc": "Telemetridata plats",
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
"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.",
"appSettings_initialRouteWeight": "Initial vikt för rutt",
"appSettings_routeWeightSuccessIncrement": "Ökning av vikt för framgång",
"appSettings_routeWeightSuccessIncrementSubtitle": "Vikt läggs till en väg efter en lyckad leverans.",
"appSettings_routeWeightFailureDecrement": "Minskning av vikten för misslyckande",
"appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök",
"appSettings_maxMessageRetries": "Maximalt antal försök",
"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",
"map_showOverlaps": "Repeater-nyckelöverlappningar",
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnreadSubtitle": "När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.",
"chat_sendCooldown": "Vänligen vänta en stund innan du skickar igen.",
"appSettings_jumpToOldestUnread": "Gå direkt till det äldsta, obesvarade meddelandet",
"appSettings_languageHu": "Ungerskt",
"appSettings_languageJa": "Japanska",
"appSettings_languageKo": "Koreanska",
"radioStats_tooltip": "Radio- och mesh-statistik",
"radioStats_screenTitle": "Radiostation",
"radioStats_notConnected": "Anslut till en enhet för att visa radiostatistik.",
"radioStats_firmwareTooOld": "Radio statistik kräver kompatibel firmware version 8 eller senare.",
"radioStats_waiting": "Väntar på data…",
"radioStats_noiseFloor": "Bakgrundsnivå: {noiseDbm} dBm",
"radioStats_lastRssi": "Senaste RSSI-värde: {rssiDbm} dBm",
"radioStats_lastSnr": "Senaste SNR: {snr} dB",
"radioStats_txAir": "TX-tid (total): {seconds} sekunder",
"radioStats_rxAir": "RX-tid (total): {seconds} s",
"radioStats_chartCaption": "Ljudnivå (dBm) baserat på de senaste mätningarna.",
"radioStats_stripNoise": "Bakgrundsnivå: {noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"@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": "BluetoothparningsPIN",
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
"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})."
}
+463 -134
View File
File diff suppressed because it is too large Load Diff
+351 -1
View File
@@ -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": "已发送广播",
@@ -300,6 +302,7 @@
"contacts_newGroup": "新建群聊",
"contacts_groupName": "群聊名称",
"contacts_groupNameRequired": "请输入群聊名称",
"contacts_groupNameReserved": "该群组名称已被保留",
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1609,6 +1612,8 @@
"map_removeLast": "移除最后一个",
"map_runTrace": "运行路径追踪",
"scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备",
"scanner_chromeRequired": "需要 Chrome 浏览器",
"scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。",
"scanner_bluetoothOff": "蓝牙已关闭",
"scanner_enableBluetooth": "启用蓝牙",
"snrIndicator_lastSeen": "最近访问",
@@ -1804,5 +1809,350 @@
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
"contacts_searchContactsNoNumber": "搜索联系人...",
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
"contacts_searchFavorites": "搜索 {number}{str} 收藏..."
"contacts_searchFavorites": "搜索 {number}{str} 收藏...",
"settings_contactSettings": "联系人设置",
"contactsSettings_title": "联系人设置",
"contactsSettings_autoAddUsersTitle": "自动添加用户",
"contactsSettings_otherTitle": "其他联系人相关设置",
"contactsSettings_autoAddUsersSubtitle": "允许伴侣自动添加发现的用户",
"contactsSettings_autoAddRepeatersSubtitle": "允许伴侣自动添加发现的重复器",
"contactsSettings_autoAddSensorsTitle": "自动添加传感器",
"contactsSettings_autoAddRoomServersSubtitle": "允许伴侣自动添加发现的房间服务器",
"contactsSettings_autoAddRepeatersTitle": "自动添加重复器",
"contactsSettings_autoAddTitle": "自动发现",
"settings_contactSettingsSubtitle": "添加联系人的设置",
"contactsSettings_overwriteOldestTitle": "覆盖最旧的",
"contactsSettings_autoAddSensorsSubtitle": "允许伴侣自动添加发现的传感器",
"discoveredContacts_searchHint": "搜索已发现的联系人",
"contactsSettings_autoAddRoomServersTitle": "自动添加房间服务器",
"discoveredContacts_contactAdded": "联系人已添加",
"discoveredContacts_deleteContact": "删除联系人",
"discoveredContacts_addContact": "添加联系人",
"discoveredContacts_noMatching": "没有匹配的联系人",
"discoveredContacts_Title": "已发现的联系人",
"discoveredContacts_copyContact": "复制联系人到剪贴板",
"contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。",
"common_deleteAll": "删除全部",
"discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?",
"discoveredContacts_deleteContactAll": "删除所有发现的联系人",
"map_showGuessedLocations": "显示猜测的节点位置",
"map_guessedLocation": "猜测的位置",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "通过USB连接",
"usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。",
"connectionChoiceBluetoothLabel": "蓝牙",
"usbScreenStatus": "选择一个 USB 设备",
"usbScreenNote": "USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。",
"usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。",
"usbErrorPermissionDenied": "拒绝了USB权限。",
"usbErrorDeviceMissing": "所选的USB设备已不再可用。",
"usbErrorInvalidPort": "选择一个有效的USB设备。",
"usbErrorBusy": "还有一个 USB 连接请求正在进行中。",
"usbErrorNotConnected": "没有连接任何USB设备。",
"usbErrorOpenFailed": "未能打开所选的USB设备。",
"usbErrorConnectFailed": "未能连接到所选的USB设备。",
"usbErrorUnsupported": "此平台不支持USB串行通信。",
"usbErrorAlreadyActive": "USB 连接已建立。",
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
"usbErrorPortClosed": "USB 连接未建立。",
"usbFallbackDeviceName": "Web 串流设备",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "正在搜索 USB 设备...",
"usbStatus_connecting": "连接USB设备...",
"usbStatus_notConnected": "选择一个 USB 设备",
"usbConnectionFailed": "USB 连接失败:{error}",
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "IP地址",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "通过 TCP 连接",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "端口",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "输入目标地址,然后连接",
"tcpStatus_connectingTo": "连接到 {endpoint}...",
"tcpErrorHostRequired": "需要提供IP地址。",
"tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。",
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
"tcpErrorTimedOut": "TCP 连接超时。",
"tcpConnectionFailed": "TCP 连接失败:{error}",
"map_showDiscoveryContacts": "显示发现联系人",
"map_setAsMyLocation": "设置为我的位置",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySubtitle": "控制要共享的信息。",
"settings_privacySettingsDescription": "选择您的设备与他人共享的信息。",
"settings_telemetryBaseMode": "遥测基础模式",
"settings_telemetryLocationMode": "遥测位置模式",
"settings_advertLocation": "广告位置",
"settings_advertLocationSubtitle": "在广告中包含位置",
"settings_allowByContact": "按联系人标志允许",
"settings_denyAll": "拒绝所有",
"settings_privacy": "隐私设置",
"settings_allowAll": "允许全部",
"contact_info": "联系信息",
"contact_teleBase": "遥测基站",
"contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据",
"settings_telemetryEnvironmentMode": "遥测环境模式",
"contact_teleLoc": "遥测位置",
"contact_teleEnv": "遥测环境",
"contact_teleEnvSubtitle": "允许共享环境传感器数据",
"contact_clearChat": "清除聊天记录",
"contact_lastSeen": "最近出现",
"contact_settings": "联系人设置",
"contact_teleLocSubtitle": "允许共享位置数据",
"contact_telemetry": "遥测数据",
"appSettings_maxRouteWeight": "最大路径重量",
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
"appSettings_initialRouteWeight": "初始路线权重",
"appSettings_maxRouteWeightSubtitle": "一条路径可以累积的最大重量,取决于成功交付的数量。",
"appSettings_routeWeightSuccessIncrement": "成功权重增加",
"appSettings_routeWeightSuccessIncrementSubtitle": "在成功交付后,将重量添加到路径中",
"appSettings_routeWeightFailureDecrement": "失败权重降低",
"appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。",
"appSettings_maxMessageRetries": "最大消息重试次数",
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "多重ACK",
"settings_telemetryModeUpdated": "遥测模式已更新",
"map_showOverlaps": "重复键重叠",
"map_runTraceWithReturnPath": "沿着相同的路径返回",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "请稍等片刻后再尝试发送。",
"appSettings_jumpToOldestUnreadSubtitle": "在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。",
"appSettings_jumpToOldestUnread": "跳转到最旧、未读的文章",
"appSettings_languageHu": "匈牙利",
"appSettings_languageJa": "日语",
"appSettings_languageKo": "韩语",
"radioStats_tooltip": "无线电和网状结构统计数据",
"radioStats_screenTitle": "广播统计数据",
"radioStats_notConnected": "连接到设备以查看收音机统计信息。",
"radioStats_firmwareTooOld": "使用无线电统计功能需要配合使用 v8 或更高版本的固件。",
"radioStats_waiting": "正在等待数据…",
"radioStats_noiseFloor": "噪声水平:{noiseDbm} dBm",
"radioStats_lastRssi": "上次 RSSI 值:{rssiDbm} dBm",
"radioStats_lastSnr": "上次 SNR{snr} dB",
"radioStats_txAir": "TX 频道播出时间(总时长):{seconds} 秒",
"radioStats_rxAir": "RX 使用时长(总时长):{seconds} 秒",
"radioStats_chartCaption": "近期的噪声水平(dBm)。",
"radioStats_stripNoise": "噪声水平:{noiseDbm} dBm",
"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": {
"type": "String"
}
}
},
"scanner_linuxPairingPinTitle": "蓝牙配对 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})."
}
+36
View File
@@ -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);
}
}
+32 -1
View File
@@ -4,6 +4,9 @@ import 'package:flutter/foundation.dart';
import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'screens/chrome_required_screen.dart';
import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart';
import 'screens/scanner_screen.dart';
import 'services/storage_service.dart';
@@ -16,6 +19,9 @@ 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';
import 'utils/app_logger.dart';
@@ -36,6 +42,9 @@ void main() async {
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
final translationService = TranslationService(appSettingsService);
final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage);
// Load settings
await appSettingsService.loadSettings();
@@ -50,18 +59,26 @@ 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();
// Wire up connector with services
connector.initialize(
retryService: retryService,
pathHistoryService: pathHistoryService,
appSettingsService: appSettingsService,
translationService: translationService,
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
backgroundService: backgroundService,
timeoutPredictionService: timeoutPredictionService,
);
await connector.loadContactCache();
@@ -83,6 +100,9 @@ void main() async {
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
translationService: translationService,
uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService,
),
);
}
@@ -118,6 +138,9 @@ class MeshCoreApp extends StatelessWidget {
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
final TranslationService translationService;
final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService;
const MeshCoreApp({
super.key,
@@ -130,6 +153,9 @@ class MeshCoreApp extends StatelessWidget {
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
required this.translationService,
required this.uiViewStateService,
required this.timeoutPredictionService,
});
@override
@@ -143,8 +169,11 @@ 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),
ChangeNotifierProvider.value(value: timeoutPredictionService),
],
child: Consumer<AppSettingsService>(
builder: (context, settingsService, child) {
@@ -187,7 +216,9 @@ class MeshCoreApp extends StatelessWidget {
NotificationService().setLocale(locale);
return child ?? const SizedBox.shrink();
},
home: const ScannerScreen(),
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
: const ScannerScreen(),
);
},
),
+137 -1
View File
@@ -1,3 +1,5 @@
import 'translation_support.dart';
enum UnitSystem { metric, imperial }
extension UnitSystemValue on UnitSystem {
@@ -18,10 +20,12 @@ class AppSettings {
final bool mapShowRepeaters;
final bool mapShowChatNodes;
final bool mapShowOtherNodes;
final bool mapShowOverlaps;
final double mapTimeFilterHours; // 0 = all time
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
final bool mapShowMarkers;
final bool mapShowGuessedLocations;
final bool enableMessageTracing;
final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom;
@@ -31,6 +35,11 @@ class AppSettings {
final bool notifyOnNewChannelMessage;
final bool notifyOnNewAdvert;
final bool autoRouteRotationEnabled;
final double maxRouteWeight;
final double initialRouteWeight;
final double routeWeightSuccessIncrement;
final double routeWeightFailureDecrement;
final int maxMessageRetries;
final String themeMode;
final String? languageOverride; // null = system default
final bool appDebugLogEnabled;
@@ -38,16 +47,28 @@ class AppSettings {
final Map<String, String> batteryChemistryByRepeaterId;
final UnitSystem unitSystem;
final Set<String> mutedChannels;
final bool mapShowDiscoveryContacts;
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,
this.mapShowRepeaters = true,
this.mapShowChatNodes = true,
this.mapShowOtherNodes = true,
this.mapShowOverlaps = false,
this.mapTimeFilterHours = 0, // Default to all time
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
this.mapShowMarkers = true,
this.mapShowGuessedLocations = true,
this.enableMessageTracing = false,
this.mapCacheBounds,
this.mapCacheMinZoom = 10,
@@ -57,6 +78,11 @@ class AppSettings {
this.notifyOnNewChannelMessage = true,
this.notifyOnNewAdvert = true,
this.autoRouteRotationEnabled = false,
this.maxRouteWeight = 5.0,
this.initialRouteWeight = 3.0,
this.routeWeightSuccessIncrement = 0.5,
this.routeWeightFailureDecrement = 0.2,
this.maxMessageRetries = 5,
this.themeMode = 'system',
this.languageOverride,
this.appDebugLogEnabled = false,
@@ -64,9 +90,20 @@ class AppSettings {
Map<String, String>? batteryChemistryByRepeaterId,
this.unitSystem = UnitSystem.metric,
Set<String>? mutedChannels,
this.mapShowDiscoveryContacts = true,
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 {
@@ -74,10 +111,12 @@ class AppSettings {
'map_show_repeaters': mapShowRepeaters,
'map_show_chat_nodes': mapShowChatNodes,
'map_show_other_nodes': mapShowOtherNodes,
'map_show_overlaps': mapShowOverlaps,
'map_time_filter_hours': mapTimeFilterHours,
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
'map_show_markers': mapShowMarkers,
'map_show_guessed_locations': mapShowGuessedLocations,
'enable_message_tracing': enableMessageTracing,
'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom,
@@ -87,6 +126,11 @@ class AppSettings {
'notify_on_new_channel_message': notifyOnNewChannelMessage,
'notify_on_new_advert': notifyOnNewAdvert,
'auto_route_rotation_enabled': autoRouteRotationEnabled,
'max_route_weight': maxRouteWeight,
'initial_route_weight': initialRouteWeight,
'route_weight_success_increment': routeWeightSuccessIncrement,
'route_weight_failure_decrement': routeWeightFailureDecrement,
'max_message_retries': maxMessageRetries,
'theme_mode': themeMode,
'language_override': languageOverride,
'app_debug_log_enabled': appDebugLogEnabled,
@@ -94,6 +138,18 @@ class AppSettings {
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
'unit_system': unitSystem.value,
'muted_channels': mutedChannels.toList(),
'map_show_discovery_contacts': mapShowDiscoveryContacts,
'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(),
};
}
@@ -110,11 +166,14 @@ class AppSettings {
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
mapTimeFilterHours:
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
mapShowGuessedLocations:
json['map_show_guessed_locations'] as bool? ?? true,
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
@@ -128,6 +187,14 @@ class AppSettings {
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
autoRouteRotationEnabled:
json['auto_route_rotation_enabled'] as bool? ?? false,
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
initialRouteWeight:
(json['initial_route_weight'] as num?)?.toDouble() ?? 3.0,
routeWeightSuccessIncrement:
(json['route_weight_success_increment'] as num?)?.toDouble() ?? 0.5,
routeWeightFailureDecrement:
(json['route_weight_failure_decrement'] as num?)?.toDouble() ?? 0.2,
maxMessageRetries: json['max_message_retries'] as int? ?? 5,
themeMode: json['theme_mode'] as String? ?? 'system',
languageOverride: json['language_override'] as String?,
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
@@ -147,6 +214,29 @@ class AppSettings {
?.map((e) => e.toString())
.toSet()) ??
{},
mapShowDiscoveryContacts:
json['map_show_discovery_contacts'] as bool? ?? true,
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 [],
);
}
@@ -155,10 +245,12 @@ class AppSettings {
bool? mapShowRepeaters,
bool? mapShowChatNodes,
bool? mapShowOtherNodes,
bool? mapShowOverlaps,
double? mapTimeFilterHours,
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
bool? mapShowMarkers,
bool? mapShowGuessedLocations,
bool? enableMessageTracing,
Object? mapCacheBounds = _unset,
int? mapCacheMinZoom,
@@ -168,6 +260,11 @@ class AppSettings {
bool? notifyOnNewChannelMessage,
bool? notifyOnNewAdvert,
bool? autoRouteRotationEnabled,
double? maxRouteWeight,
double? initialRouteWeight,
double? routeWeightSuccessIncrement,
double? routeWeightFailureDecrement,
int? maxMessageRetries,
String? themeMode,
Object? languageOverride = _unset,
bool? appDebugLogEnabled,
@@ -175,16 +272,29 @@ class AppSettings {
Map<String, String>? batteryChemistryByRepeaterId,
UnitSystem? unitSystem,
Set<String>? mutedChannels,
bool? mapShowDiscoveryContacts,
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,
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
mapShowGuessedLocations:
mapShowGuessedLocations ?? this.mapShowGuessedLocations,
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds
@@ -198,6 +308,13 @@ class AppSettings {
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoRouteRotationEnabled:
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
initialRouteWeight: initialRouteWeight ?? this.initialRouteWeight,
routeWeightSuccessIncrement:
routeWeightSuccessIncrement ?? this.routeWeightSuccessIncrement,
routeWeightFailureDecrement:
routeWeightFailureDecrement ?? this.routeWeightFailureDecrement,
maxMessageRetries: maxMessageRetries ?? this.maxMessageRetries,
themeMode: themeMode ?? this.themeMode,
languageOverride: languageOverride == _unset
? this.languageOverride
@@ -209,6 +326,25 @@ class AppSettings {
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
unitSystem: unitSystem ?? this.unitSystem,
mutedChannels: mutedChannels ?? this.mutedChannels,
mapShowDiscoveryContacts:
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
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,
);
}
}
+12 -9
View File
@@ -24,20 +24,23 @@ class Channel {
bool get isPublicChannel => pskHex == publicChannelPsk;
static Channel? fromFrame(Uint8List data) {
static Channel? fromFrame(Uint8List frame) {
// CHANNEL_INFO format:
// [0] = RESP_CODE_CHANNEL_INFO (18)
// [1] = channel_idx
// [2-33] = name (32 bytes, null-terminated)
// [34-49] = psk (16 bytes)
if (data.length < 50) return null;
if (data[0] != respCodeChannelInfo) return null;
final index = data[1];
final name = readCString(data, 2, 32);
final psk = Uint8List.fromList(data.sublist(34, 50));
return Channel(index: index, name: name, psk: psk);
if (frame.length < 50) return null;
final reader = BufferReader(frame);
try {
if (reader.readByte() != respCodeChannelInfo) return null;
final index = reader.readByte();
final name = reader.readCStringGreedy(32);
final psk = reader.readBytes(16);
return Channel(index: index, name: name, psk: psk);
} catch (e) {
return null;
}
}
static Channel empty(int index) {
+114 -79
View File
@@ -2,6 +2,8 @@ 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 }
@@ -23,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;
@@ -36,6 +45,7 @@ class ChannelMessage {
final List<Uint8List> pathVariants;
final int? channelIndex;
final String messageId;
final String? packetHash;
final String? replyToMessageId;
final String? replyToSenderName;
final String? replyToText;
@@ -45,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,
@@ -55,6 +70,7 @@ class ChannelMessage {
List<Uint8List>? pathVariants,
this.channelIndex,
String? messageId,
this.packetHash,
this.replyToMessageId,
this.replyToSenderName,
this.replyToText,
@@ -79,15 +95,34 @@ class ChannelMessage {
int? pathLength,
Uint8List? pathBytes,
List<Uint8List>? pathVariants,
String? packetHash,
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,
@@ -98,6 +133,7 @@ class ChannelMessage {
pathVariants: pathVariants ?? this.pathVariants,
channelIndex: channelIndex,
messageId: messageId,
packetHash: packetHash ?? this.packetHash,
replyToMessageId: replyToMessageId ?? this.replyToMessageId,
replyToSenderName: replyToSenderName ?? this.replyToSenderName,
replyToText: replyToText ?? this.replyToText,
@@ -105,100 +141,99 @@ class ChannelMessage {
);
}
static ChannelMessage? fromFrame(Uint8List data) {
static ChannelMessage? fromFrame(Uint8List frame) {
// CHANNEL_MSG_RECV format varies by version:
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
if (data.length < 8) return null;
if (frame.length < 8) return null;
try {
final reader = BufferReader(frame);
final code = reader.readByte();
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
return null;
}
final code = data[0];
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
int pathLen;
int txtType;
Uint8List pathBytes = Uint8List(0);
int channelIdx;
if (code == respCodeChannelMsgRecvV3) {
reader.skipBytes(1); // Skip SNR
final flags = reader.readByte();
final hasPath = (flags & 0x01) != 0;
reader.skipBytes(1); // Skip reserved byte
channelIdx = reader.readByte();
pathLen = reader.readInt8();
txtType = reader.readByte();
if (hasPath && pathLen > 0) {
reader.rewind(); // Rewind to read path length again for pathBytes
pathBytes = reader.readBytes(pathLen);
}
} else {
channelIdx = reader.readByte();
pathLen = reader.readInt8();
txtType = reader.readByte();
}
final timestampRaw = reader.readUInt32LE();
if (txtType != txtTypePlain) {
return null;
}
final text = reader.readCString();
// Extract sender name and actual message from "name: msg" format
String senderName = 'Unknown';
String actualText = text;
final colonIndex = text.indexOf(':');
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
final potentialSender = text.substring(0, colonIndex);
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
senderName = potentialSender;
final offset =
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
? colonIndex + 2
: colonIndex + 1;
actualText = text.substring(offset);
}
}
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: decodedText,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
status: ChannelMessageStatus.sent,
pathLength: pathLen,
pathBytes: pathBytes,
channelIndex: channelIdx,
);
} catch (e) {
appLogger.error('Error parsing channel message frame: $e');
// If parsing fails, return null to avoid crashes
return null;
}
int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
Uint8List pathBytes = Uint8List(0);
int channelIdx;
if (code == respCodeChannelMsgRecvV3) {
channelIdx = data[4];
pathLenOffset = 5;
final pathLen = data[pathLenOffset].toSigned(8);
var cursor = 6;
final hasPathBytesFlag = (data[2] & 0x01) != 0;
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
final hasValidTxtType =
cursor < data.length &&
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
canFitPath) {
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
cursor += pathLen;
}
txtTypeOffset = cursor;
cursor += 1; // txt_type
timestampOffset = cursor;
textOffset = cursor + 4;
} else {
channelIdx = data[1];
pathLenOffset = 2;
txtTypeOffset = 3;
timestampOffset = 4;
textOffset = 8;
}
if (data.length < textOffset + 1) return null;
final txtType = data[txtTypeOffset];
if (txtType != txtTypePlain) {
return null;
}
final pathLen = data[pathLenOffset].toSigned(8);
final timestampRaw = readUint32LE(data, timestampOffset);
final text = readCString(data, textOffset, data.length - textOffset);
// Extract sender name and actual message from "name: msg" format
String senderName = 'Unknown';
String actualText = text;
final colonIndex = text.indexOf(':');
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
final potentialSender = text.substring(0, colonIndex);
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
senderName = potentialSender;
final offset =
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
? colonIndex + 2
: colonIndex + 1;
actualText = text.substring(offset);
}
}
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: decodedText,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
status: ChannelMessageStatus.sent,
pathLength: pathLen,
pathBytes: pathBytes,
channelIndex: channelIdx,
);
}
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,
+48
View File
@@ -0,0 +1,48 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart';
/// Parsed `RESP_CODE_STATS` + `STATS_TYPE_RADIO` (14 bytes total).
class CompanionRadioStats {
final int noiseFloorDbm;
final int lastRssiDbm;
final double lastSnrDb;
final int txAirSecs;
final int rxAirSecs;
final DateTime receivedAt;
const CompanionRadioStats({
required this.noiseFloorDbm,
required this.lastRssiDbm,
required this.lastSnrDb,
required this.txAirSecs,
required this.rxAirSecs,
required this.receivedAt,
});
static CompanionRadioStats? tryParse(Uint8List frame) {
if (frame.length < 14) return null;
if (frame[0] != respCodeStats || frame[1] != statsTypeRadio) return null;
try {
final reader = BufferReader(frame);
reader.skipBytes(2);
final noise = reader.readInt16LE();
final rssi = reader.readInt8();
final snrRaw = reader.readInt8();
final txAir = reader.readUInt32LE();
final rxAir = reader.readUInt32LE();
return CompanionRadioStats(
noiseFloorDbm: noise,
lastRssiDbm: rssi,
lastSnrDb: snrRaw / 4.0,
txAirSecs: txAir,
rxAirSecs: rxAir,
receivedAt: DateTime.now(),
);
} catch (e) {
appLogger.warn('CompanionRadioStats parse error: $e');
return null;
}
}
}
+100 -77
View File
@@ -1,4 +1,6 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class Contact {
@@ -15,6 +17,10 @@ class Contact {
final double? longitude;
final DateTime lastSeen;
final DateTime lastMessageAt;
final DateTime? lastModified;
final bool isActive;
final bool wasPulled;
final Uint8List? rawPacket;
Contact({
required this.publicKey,
@@ -28,12 +34,19 @@ class Contact {
this.latitude,
this.longitude,
required this.lastSeen,
this.lastModified,
DateTime? lastMessageAt,
this.isActive = true,
this.wasPulled = false,
this.rawPacket,
}) : lastMessageAt = lastMessageAt ?? lastSeen;
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';
@@ -48,18 +61,17 @@ 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;
final lon = longitude ?? 0.0;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
bool get hasLocation => latitude != null && longitude != null;
bool get isFavorite => (flags & contactFlagFavorite) != 0;
Contact copyWith({
@@ -76,6 +88,9 @@ class Contact {
double? longitude,
DateTime? lastSeen,
DateTime? lastMessageAt,
DateTime? lastModified,
bool? isActive,
Uint8List? rawPacket,
}) {
return Contact(
publicKey: publicKey ?? this.publicKey,
@@ -94,18 +109,20 @@ 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,
);
}
String get pathIdList {
final pathBytes = _pathBytesForDisplay;
/// Formats path bytes into comma-separated hex groups of [hashByteWidth] bytes.
String pathFormattedIdList(int hashByteWidth) {
final pathBytes = pathBytesForDisplay;
if (pathBytes.isEmpty) return '';
final w = hashByteWidth.clamp(1, 8);
final parts = <String>[];
final groupSize = pathHashSize;
for (int i = 0; i < pathBytes.length; i += groupSize) {
final end = (i + groupSize) <= pathBytes.length
? (i + groupSize)
: pathBytes.length;
for (int i = 0; i < pathBytes.length; i += w) {
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
final chunk = pathBytes.sublist(i, end);
parts.add(
chunk
@@ -116,47 +133,14 @@ class Contact {
return parts.join(',');
}
/// Default grouping uses legacy single-byte hop hash width.
String get pathIdList => pathFormattedIdList(pathHashSize);
String get shortPubKeyHex {
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
}
Uint8List? get traceRouteBytes {
final pathBytes = _pathBytesForDisplay;
Uint8List? traceBytes;
if (pathBytes.isEmpty) {
traceBytes = Uint8List(1);
traceBytes[0] = publicKey[0];
return traceBytes;
}
if (type == advTypeRepeater || type == advTypeRoom) {
final len = (pathBytes.length + pathBytes.length + 1);
traceBytes = Uint8List(len);
traceBytes[pathBytes.length] = publicKey[0];
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
} else {
if (pathBytes.length < 2) {
return pathBytes[0] == 0 ? null : pathBytes;
}
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
}
return traceBytes;
}
Uint8List get _pathBytesForDisplay {
Uint8List get pathBytesForDisplay {
if (pathOverride != null) {
if (pathOverride! < 0) return Uint8List(0);
return pathOverrideBytes ?? Uint8List(0);
@@ -166,31 +150,62 @@ class Contact {
static Contact? fromFrame(Uint8List data) {
if (data.isEmpty) return null;
if (data[0] != respCodeContact) return null;
final reader = BufferReader(data);
try {
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final flags = data[contactFlagsOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final respCode = reader.readByte();
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
return null;
}
final pubKey = reader.readBytes(pubKeySize);
// Guard: reject contacts with zeroed or mostly-zeroed public keys
// (indicates corrupt flash storage on the firmware side)
final zeroCount = pubKey.where((b) => b == 0).length;
if (zeroCount > pubKeySize ~/ 2) return null;
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastmodOffset);
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);
// Guard: reject contacts with non-printable names (corrupt flash data)
if (name.isNotEmpty &&
name.codeUnits.every((c) => c < 0x20 || c == 0xFFFD)) {
return null;
}
// mandatory last_advert_timestamp
final lastAdvertTimestamp = reader.readUInt32LE();
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
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(
@@ -198,14 +213,19 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen,
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
lastSeen: DateTime.fromMillisecondsSinceEpoch(
lastAdvertTimestamp * 1000,
),
lastModified: lastModified,
isActive: true,
rawPacket: null,
);
} catch (e) {
// If parsing fails, return null
appLogger.error('Failed to parse contact frame: $e');
return null;
}
}
@@ -216,4 +236,7 @@ class Contact {
@override
int get hashCode => publicKeyHex.hashCode;
bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
}
+43
View File
@@ -0,0 +1,43 @@
class DeliveryObservation {
final String contactKey;
final int pathLength;
final int messageBytes;
final int secondsSinceLastRx;
final bool isFlood;
final int deliveryMs;
final DateTime timestamp;
DeliveryObservation({
required this.contactKey,
required this.pathLength,
required this.messageBytes,
required this.secondsSinceLastRx,
required this.isFlood,
required this.deliveryMs,
required this.timestamp,
});
Map<String, dynamic> toJson() {
return {
'contact_key': contactKey,
'path_length': pathLength,
'message_bytes': messageBytes,
'seconds_since_last_rx': secondsSinceLastRx,
'is_flood': isFlood,
'delivery_ms': deliveryMs,
'timestamp': timestamp.toIso8601String(),
};
}
factory DeliveryObservation.fromJson(Map<String, dynamic> json) {
return DeliveryObservation(
contactKey: json['contact_key'] as String,
pathLength: json['path_length'] as int,
messageBytes: json['message_bytes'] as int,
secondsSinceLastRx: json['seconds_since_last_rx'] as int? ?? 0,
isFlood: json['is_flood'] as bool,
deliveryMs: json['delivery_ms'] as int,
timestamp: DateTime.parse(json['timestamp'] as String),
);
}
}
+77 -30
View File
@@ -1,28 +1,37 @@
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 Uint8List? expectedAckHash;
final int? expectedAckHash;
final DateTime? sentAt;
final DateTime? deliveredAt;
final int? tripTimeMs;
final int? pathLength;
final Uint8List pathBytes;
final Map<String, int> reactions;
final Map<String, MessageStatus> reactionStatuses;
final Uint8List fourByteRoomContactKey;
Message({
@@ -32,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,
@@ -43,9 +57,14 @@ class Message {
Uint8List? pathBytes,
Uint8List? fourByteRoomContactKey,
Map<String, int>? reactions,
}) : pathBytes = pathBytes ?? Uint8List(0),
Map<String, MessageStatus>? reactionStatuses,
}) : messageId =
messageId ??
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
reactions = reactions ?? {};
reactions = reactions ?? {},
reactionStatuses = reactionStatuses ?? {};
String get senderKeyHex => pubKeyToHex(senderKey);
@@ -53,14 +72,20 @@ class Message {
MessageStatus? status,
int? retryCount,
int? estimatedTimeoutMs,
Uint8List? expectedAckHash,
int? expectedAckHash,
DateTime? sentAt,
DateTime? deliveredAt,
int? tripTimeMs,
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,
}) {
return Message(
@@ -71,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,
@@ -80,49 +118,58 @@ class Message {
pathLength: pathLength ?? this.pathLength,
pathBytes: pathBytes ?? this.pathBytes,
reactions: reactions ?? this.reactions,
reactionStatuses: reactionStatuses ?? this.reactionStatuses,
fourByteRoomContactKey:
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
);
}
static Message? fromFrame(Uint8List data, Uint8List selfPubKey) {
if (data.length < msgTextOffset + 1) return null;
static Message? fromFrame(Uint8List frame, Uint8List selfPubKey) {
if (frame.length < msgTextOffset + 1) return null;
final reader = BufferReader(frame);
try {
final code = reader.readByte();
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
return null;
}
final code = data[0];
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
final senderKey = reader.readBytes(pubKeySize);
final timestampRaw = reader.readInt32LE();
final flags = reader.readByte();
if ((flags >> 2) != txtTypePlain) {
return null;
}
final text = reader.readCString();
return Message(
senderKey: senderKey,
text: text,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
isCli: false,
status: MessageStatus.delivered,
pathBytes: Uint8List(0),
);
} catch (e) {
return null;
}
final senderKey = Uint8List.fromList(
data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
);
final timestampRaw = readUint32LE(data, msgTimestampOffset);
final flags = data[msgFlagsOffset];
if ((flags >> 2) != txtTypePlain) {
return null;
}
final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
return Message(
senderKey: senderKey,
text: text,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
isCli: false,
status: MessageStatus.delivered,
pathBytes: Uint8List(0),
);
}
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,
+9 -3
View File
@@ -1,11 +1,12 @@
class PathRecord {
final int hopCount;
final int tripTimeMs;
final DateTime timestamp;
final DateTime? timestamp;
final bool wasFloodDiscovery;
final List<int> pathBytes;
final int successCount;
final int failureCount;
final double routeWeight;
PathRecord({
required this.hopCount,
@@ -15,6 +16,7 @@ class PathRecord {
required this.pathBytes,
required this.successCount,
required this.failureCount,
this.routeWeight = 1.0,
});
String get displayText =>
@@ -24,11 +26,12 @@ class PathRecord {
return {
'hop_count': hopCount,
'trip_time_ms': tripTimeMs,
'timestamp': timestamp.toIso8601String(),
'timestamp': timestamp?.toIso8601String(),
'was_flood': wasFloodDiscovery,
'path_bytes': pathBytes,
'success_count': successCount,
'failure_count': failureCount,
'route_weight': routeWeight,
};
}
@@ -36,12 +39,15 @@ class PathRecord {
return PathRecord(
hopCount: json['hop_count'] as int,
tripTimeMs: json['trip_time_ms'] as int,
timestamp: DateTime.parse(json['timestamp'] as String),
timestamp: json['timestamp'] != null
? DateTime.parse(json['timestamp'] as String)
: null,
wasFloodDiscovery: json['was_flood'] as bool,
pathBytes:
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
);
}
}
+41
View File
@@ -1,3 +1,9 @@
import 'dart:typed_data';
import 'contact.dart';
const int recentAttemptDiversityWindow = 2;
class PathSelection {
final List<int> pathBytes;
final int hopCount;
@@ -9,3 +15,38 @@ class PathSelection {
required this.useFlood,
});
}
PathSelection resolvePathSelection(
Contact contact, {
PathSelection? selection,
bool forceFlood = false,
}) {
if (contact.pathOverride != null) {
if (contact.pathOverride! < 0) {
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
}
return PathSelection(
pathBytes: contact.pathOverrideBytes ?? Uint8List(0),
hopCount: contact.pathOverride!,
useFlood: false,
);
}
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
}
if (selection != null && selection.pathBytes.isNotEmpty) {
return PathSelection(
pathBytes: selection.pathBytes,
hopCount: selection.hopCount,
useFlood: false,
);
}
return PathSelection(
pathBytes: contact.path,
hopCount: contact.pathLength,
useFlood: false,
);
}
+3 -3
View File
@@ -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,
),
),
+136
View File
@@ -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,
),
];
+4 -2
View File
@@ -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,
+735 -72
View File
@@ -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,19 +300,26 @@ 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),
);
},
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.vertical_align_top),
title: Text(context.l10n.appSettings_jumpToOldestUnread),
subtitle: Text(context.l10n.appSettings_jumpToOldestUnreadSubtitle),
value: settingsService.settings.jumpToOldestUnread,
onChanged: settingsService.setJumpToOldestUnread,
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.alt_route),
title: Text(context.l10n.appSettings_autoRouteRotation),
@@ -298,18 +327,129 @@ 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),
);
},
),
if (settingsService.settings.autoRouteRotationEnabled) ...[
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_maxRouteWeight),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_maxRouteWeightSubtitle),
Slider(
value: settingsService.settings.maxRouteWeight,
min: 1,
max: 10,
divisions: 9,
label: settingsService.settings.maxRouteWeight
.round()
.toString(),
onChanged: (value) =>
settingsService.setMaxRouteWeight(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_initialRouteWeight),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_initialRouteWeightSubtitle),
Slider(
value: settingsService.settings.initialRouteWeight,
min: 0.5,
max: 5.0,
divisions: 9,
label: settingsService.settings.initialRouteWeight
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setInitialRouteWeight(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_routeWeightSuccessIncrement),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context
.l10n
.appSettings_routeWeightSuccessIncrementSubtitle,
),
Slider(
value: settingsService.settings.routeWeightSuccessIncrement,
min: 0.1,
max: 2.0,
divisions: 19,
label: settingsService.settings.routeWeightSuccessIncrement
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setRouteWeightSuccessIncrement(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_routeWeightFailureDecrement),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context
.l10n
.appSettings_routeWeightFailureDecrementSubtitle,
),
Slider(
value: settingsService.settings.routeWeightFailureDecrement,
min: 0.1,
max: 2.0,
divisions: 19,
label: settingsService.settings.routeWeightFailureDecrement
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setRouteWeightFailureDecrement(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_maxMessageRetries),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_maxMessageRetriesSubtitle),
Slider(
value: settingsService.settings.maxMessageRetries
.toDouble(),
min: 2,
max: 10,
divisions: 8,
label: settingsService.settings.maxMessageRetries
.toString(),
onChanged: (value) =>
settingsService.setMaxMessageRetries(value.toInt()),
),
],
),
),
],
],
),
);
@@ -410,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,
@@ -577,6 +922,12 @@ class AppSettingsScreen extends StatelessWidget {
return context.l10n.appSettings_languageRu;
case 'uk':
return context.l10n.appSettings_languageUk;
case 'hu':
return context.l10n.appSettings_languageHu;
case 'ja':
return context.l10n.appSettings_languageJa;
case 'ko':
return context.l10n.appSettings_languageKo;
default:
return context.l10n.appSettings_languageSystem;
}
@@ -664,6 +1015,18 @@ class AppSettingsScreen extends StatelessWidget {
title: Text(context.l10n.appSettings_languageUk),
value: 'uk',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageHu),
value: 'hu',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageJa),
value: 'ja',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageKo),
value: 'ko',
),
],
),
),
@@ -699,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,
),
],
),
@@ -751,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,
),
],
),
@@ -772,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,
@@ -795,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),
);
},
),
@@ -812,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),
),
],
);
}
}
+72 -59
View File
@@ -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,
@@ -118,6 +118,19 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
: Icons.download,
size: 18,
),
onLongPress: () async {
await Clipboard.setData(
ClipboardData(
text: entry.payload
.map(
(b) => b
.toRadixString(16)
.padLeft(2, '0'),
)
.join(''),
),
);
},
);
}
@@ -270,66 +283,66 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
if (payload.length < 101) {
return 'ADVERT (short)';
}
var offset = 0;
final pubKey = _bytesToHex(
payload.sublist(offset, offset + 32),
spaced: false,
);
offset += 32;
final timestamp = readUint32LE(payload, offset);
offset += 4;
offset += 64; // signature
final flags = payload[offset++];
final role = _deviceRoleLabel(flags & 0x0F);
final hasLocation = (flags & 0x10) != 0;
final hasFeature1 = (flags & 0x20) != 0;
final hasFeature2 = (flags & 0x40) != 0;
final hasName = (flags & 0x80) != 0;
String? name;
double? lat;
double? lon;
if (hasLocation && payload.length >= offset + 8) {
lat = readInt32LE(payload, offset) / 1000000.0;
lon = readInt32LE(payload, offset + 4) / 1000000.0;
offset += 8;
final reader = BufferReader(payload);
try {
final pubKey = _bytesToHex(reader.readBytes(pubKeySize), spaced: false);
final timestamp = reader.readUInt32LE();
reader.skipBytes(signatureSize);
final flags = reader.readByte();
final role = _deviceRoleLabel(flags & 0x0F);
final hasLocation = (flags & 0x10) != 0;
final hasFeature1 = (flags & 0x20) != 0;
final hasFeature2 = (flags & 0x40) != 0;
final hasName = (flags & 0x80) != 0;
String? name;
double? lat;
double? lon;
if (hasLocation) {
lat = reader.readInt32LE() / 1000000.0;
lon = reader.readInt32LE() / 1000000.0;
}
if (hasFeature1) reader.skipBytes(2);
if (hasFeature2) reader.skipBytes(2);
if (hasName) {
name = reader.readCStringGreedy(maxNameSize);
}
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
final locPart = (lat != null && lon != null)
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
: '';
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}';
} catch (e) {
return 'ADVERT (invalid)';
}
if (hasFeature1) offset += 2;
if (hasFeature2) offset += 2;
if (hasName && payload.length > offset) {
final rawName = String.fromCharCodes(payload.sublist(offset));
final nul = rawName.indexOf('\u0000');
name = nul >= 0 ? rawName.substring(0, nul) : rawName;
name = name.trim();
}
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
final locPart = (lat != null && lon != null)
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
: '';
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}';
}
String _decodeControlSummary(Uint8List payload) {
if (payload.isEmpty) return 'CONTROL (empty)';
final flags = payload[0];
final subType = flags & 0xF0;
if (subType == 0x80) {
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
final typeFilter = payload[1];
final tag = readUint32LE(payload, 2);
final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0;
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
final reader = BufferReader(payload);
try {
final flags = reader.readByte();
final subType = flags & 0xF0;
if (subType == 0x80) {
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
final typeFilter = reader.readByte();
final tag = reader.readInt32LE();
final since = payload.length >= 10 ? reader.readInt32LE() : 0;
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
}
if (subType == 0x90) {
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
final nodeType = flags & 0x0F;
final snrRaw = payload[1];
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
final snr = snrSigned / 4.0;
final tag = reader.readInt32LE();
final keyLen = payload.length - 6;
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
}
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
} catch (e) {
return 'CONTROL (invalid)';
}
if (subType == 0x90) {
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
final nodeType = flags & 0x0F;
final snrRaw = payload[1];
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
final snr = snrSigned / 4.0;
final tag = readUint32LE(payload, 2);
final keyLen = payload.length - 6;
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
}
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
}
String _payloadTypeLabel(int payloadType) {
+311 -92
View File
@@ -4,35 +4,47 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_linkify/flutter_linkify.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();
@@ -47,19 +59,76 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
bool _isLoadingOlder = false;
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;
_connector = context.read<MeshCoreConnector>();
_connector?.setActiveChannel(widget.channel.index);
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final idx = widget.channel.index;
final unread = widget.initialUnreadCount;
final messages = connector.getChannelMessages(widget.channel);
ChannelMessage? anchor;
if (unread > 0) {
anchor = _findOldestUnreadChannelAnchor(messages, unread);
}
setState(() {
if (anchor != null) _unreadDividerMessageId = anchor.messageId;
});
connector.setActiveChannel(idx);
_connector = connector;
if (anchor != null && settings.jumpToOldestUnread) {
_channelSkipNextBottomSnap = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_scrollController.jumpToEstimatedOffset(
unreadCount: unread,
totalMessages: messages.length,
onJumped: () {
if (!mounted) return;
_scrollToMessage(anchor!.messageId);
},
);
});
}
});
}
ChannelMessage? _findOldestUnreadChannelAnchor(
List<ChannelMessage> messages,
int unreadCount,
) {
if (unreadCount <= 0 || messages.isEmpty) return null;
var n = 0;
ChannelMessage? oldest;
for (final m in messages.reversed) {
if (m.isOutgoing) continue;
n++;
oldest = m;
if (n >= unreadCount) break;
}
return oldest;
}
void _clearDividerAtBottom() {
if (!_scrollController.showJumpToBottom.value &&
_unreadDividerMessageId != null) {
setState(() => _unreadDividerMessageId = null);
}
}
void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
@@ -81,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();
@@ -103,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;
}
@@ -166,6 +235,34 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
),
centerTitle: false,
actions: [
const RadioStatsIconButton(),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'clearChat') {
context.read<MeshCoreConnector>().clearMessagesForChannel(
widget.channel.index,
);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'clearChat',
child: Row(
children: [
const Icon(Icons.delete, size: 20, color: Colors.red),
const SizedBox(width: 12),
Text(
context.l10n.contact_clearChat,
style: const TextStyle(color: Colors.red),
),
],
),
),
],
),
],
),
body: SafeArea(
top: false,
@@ -216,6 +313,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_channelSkipNextBottomSnap) {
_channelSkipNextBottomSnap = false;
return;
}
_scrollController.scrollToBottomIfAtBottom();
});
@@ -248,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(
@@ -256,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;
},
),
);
@@ -279,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
@@ -311,8 +442,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
Flexible(
child: GestureDetector(
onTap: () => _showMessagePathInfo(message),
onTap: PlatformInfo.isDesktop
? null
: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
onSecondaryTapUp: PlatformInfo.isDesktop
? (_) => _showMessageActions(message)
: null,
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
@@ -359,6 +495,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
poi,
isOutgoing,
textScale,
message.senderName,
trailing: (!enableTracing && isOutgoing)
? Padding(
padding: const EdgeInsets.only(bottom: 2),
@@ -430,24 +567,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Linkify(
text: message.text,
child: TranslatedMessageContent(
displayText: translatedDisplayText,
originalText: originalDisplayText,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
originalStyle: TextStyle(
fontSize: bodyFontSize * textScale,
color: Colors.green,
decoration: TextDecoration.underline,
),
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) => LinkHandler.handleLinkTap(
context,
link.url,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.72),
),
),
),
@@ -476,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],
@@ -497,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],
@@ -557,7 +689,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
);
if (!isOutgoing) {
if (!isOutgoing && !PlatformInfo.isDesktop) {
return _SwipeReplyBubble(
maxSwipeOffset: maxSwipeOffset,
replySwipeThreshold: replySwipeThreshold,
@@ -621,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) {
@@ -733,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;
@@ -776,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,
),
),
);
@@ -819,7 +943,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
isScrollControlled: true,
builder: (context) => GifPicker(
onGifSelected: (gifId) {
_textController.text = 'g:$gifId';
_textController.text = GifHelper.encodeGif(gifId);
},
),
);
@@ -933,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: [
@@ -964,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,
@@ -1013,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(),
);
},
),
@@ -1041,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,
),
@@ -1051,39 +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)) {
showDismissibleSnackBar(
context,
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;
}
}
@@ -1112,6 +1319,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_setReplyingTo(message);
},
),
if (PlatformInfo.isDesktop)
ListTile(
leading: const Icon(Icons.route),
title: Text(context.l10n.chat_path),
onTap: () {
Navigator.pop(sheetContext);
_showMessagePathInfo(message);
},
),
// Can't react to your own messages
if (!message.isOutgoing)
ListTile(
@@ -1130,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),
@@ -1171,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) {
@@ -1347,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});
}
+209 -51
View File
@@ -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';
@@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp;
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
final hops = _buildPathHops(primaryPath, connector, l10n);
final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops(
primaryPath.length,
@@ -62,8 +61,12 @@ class ChannelMessagePathScreen extends StatelessWidget {
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: primaryPath,
flipPathRound: true,
reversePathRound: !message.isOutgoing && !channelMessage,
flipPathAround: true,
reversePathAround:
!(!channelMessage && !message.isOutgoing),
pathHashByteWidth: context
.read<MeshCoreConnector>()
.pathHashByteWidth,
),
),
),
@@ -301,11 +304,15 @@ 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;
double _pathDistance = 0.0;
bool _showNodeLabels = true;
bool _didReceivePositionUpdate = false;
int? _focusedHopIndex;
@override
void initState() {
@@ -325,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();
@@ -336,6 +355,86 @@ class _ChannelMessagePathMapScreenState
return totalDistance;
}
void _focusHop(_PathHop hop) {
if (!hop.hasLocation) return;
final targetZoom = _didReceivePositionUpdate
? max(_mapController.camera.zoom, 10.0)
: 12.0;
_mapController.move(hop.position!, targetZoom);
}
void _onHopTapped(_PathHop hop) {
_focusHop(hop);
if (!mounted) return;
setState(() {
_focusedHopIndex = hop.index;
});
}
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>(
@@ -351,6 +450,7 @@ class _ChannelMessagePathMapScreenState
primaryPath,
widget.message.pathVariants,
);
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
final selectedPathTmp = _resolveSelectedPath(
_selectedPath,
observedPaths,
@@ -364,11 +464,7 @@ class _ChannelMessagePathMapScreenState
: selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths);
final hops = _buildPathHops(
selectedPath,
connector.contacts,
context.l10n,
);
final hops = _buildPathHops(selectedPath, connector, context.l10n);
final points = <LatLng>[];
@@ -423,6 +519,7 @@ class _ChannelMessagePathMapScreenState
children: [
FlutterMap(
key: mapKey,
mapController: _mapController,
options: MapOptions(
initialCenter: initialCenter,
initialZoom: initialZoom,
@@ -433,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;
@@ -468,12 +575,19 @@ class _ChannelMessagePathMapScreenState
),
],
),
if (isDesktop)
_buildDesktopMapControls(
initialCenter: initialCenter,
initialZoom: initialZoom,
bounds: bounds,
),
if (observedPaths.length > 1)
_buildPathSelector(context, observedPaths, selectedIndex, (
index,
) {
setState(() {
_selectedPath = observedPaths[index].pathBytes;
_focusedHopIndex = null;
});
}),
if (points.isEmpty)
@@ -729,8 +843,17 @@ class _ChannelMessagePathMapScreenState
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final hop = hops[index];
final isFocused = _focusedHopIndex == hop.index;
return ListTile(
dense: true,
enabled: hop.hasLocation,
selected: isFocused,
selectedTileColor: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.12),
onTap: hop.hasLocation
? () => _onHopTapped(hop)
: null,
leading: CircleAvatar(
radius: 14,
child: Text(
@@ -789,19 +912,83 @@ class _ObservedPath {
List<_PathHop> _buildPathHops(
Uint8List pathBytes,
List<Contact> contacts,
MeshCoreConnector connector,
AppLocalizations l10n,
) {
if (pathBytes.isEmpty) return const [];
final candidatesByPrefix = <int, List<Contact>>{};
final allContacts = connector.allContacts;
for (final contact in allContacts) {
if (contact.publicKey.isEmpty) continue;
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
continue;
}
final prefix = contact.publicKey.first;
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
}
for (final candidates in candidatesByPrefix.values) {
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
}
final startPoint =
(connector.selfLatitude != null && connector.selfLongitude != null)
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
: 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 prefix = pathBytes[i];
final contact = _matchContactForPrefix(contacts, prefix);
final searchPoint = i == 0 ? startPoint : previousPosition;
final candidates = candidatesByPrefix[pathBytes[i]];
Contact? contact;
if (candidates != null && candidates.isNotEmpty) {
var bestIndex = 0;
if (searchPoint != null) {
bestDistance = double.infinity;
for (var j = 0; j < candidates.length; j++) {
final candidate = candidates[j];
if (!candidate.hasLocation ||
candidate.latitude == null ||
candidate.longitude == null) {
continue;
}
final currentDistance = distance(
searchPoint,
LatLng(candidate.latitude!, candidate.longitude!),
);
if (currentDistance < bestDistance) {
bestDistance = currentDistance;
bestIndex = j;
}
}
}
contact = candidates.removeAt(bestIndex);
if (candidates.isEmpty) {
candidatesByPrefix.remove(pathBytes[i]);
}
}
final resolvedPosition = _resolvePosition(contact);
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,
prefix: prefix,
prefix: pathBytes[i],
contact: contact,
position: _resolvePosition(contact),
position: resolvedPosition,
l10n: l10n,
),
);
@@ -809,42 +996,13 @@ List<_PathHop> _buildPathHops(
return hops;
}
Contact? _matchContactForPrefix(List<Contact> contacts, int prefix) {
final matches = contacts
.where(
(contact) =>
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
contact.publicKey.isNotEmpty &&
contact.publicKey[0] == prefix,
)
.toList();
if (matches.isEmpty) return null;
Contact? pickWhere(bool Function(Contact) predicate) {
for (final contact in matches) {
if (predicate(contact)) return contact;
}
return null;
}
return pickWhere((c) => c.type == advTypeRepeater && _hasValidLocation(c)) ??
pickWhere((c) => c.type == advTypeRepeater) ??
pickWhere(_hasValidLocation) ??
matches.first;
}
LatLng? _resolvePosition(Contact? contact) {
if (contact == null) return null;
if (!_hasValidLocation(contact)) return null;
return LatLng(contact.latitude!, contact.longitude!);
}
bool _hasValidLocation(Contact contact) {
final lat = contact.latitude;
final lon = contact.longitude;
if (lat == null || lon == null) return false;
if (lat == 0 && lon == 0) return false;
return true;
if (!contact.hasLocation) return null;
final latitude = contact.latitude;
final longitude = contact.longitude;
if (latitude == null || longitude == null) return null;
return LatLng(latitude, longitude);
}
String _formatPrefix(int prefix) {
+249 -240
View File
@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/storage/channel_message_store.dart';
import 'package:meshcore_open/utils/platform_info.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
@@ -11,6 +12,7 @@ import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../services/ui_view_state_service.dart';
import '../models/channel.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
@@ -22,14 +24,13 @@ 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';
import 'map_screen.dart';
import 'settings_screen.dart';
enum ChannelSortOption { manual, name, latestMessages, unread }
class ChannelsScreen extends StatefulWidget {
final bool hideBackButton;
@@ -43,17 +44,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
with DisconnectNavigationMixin {
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
String _searchQuery = '';
Timer? _searchDebounce;
ChannelSortOption _sortOption = ChannelSortOption.manual;
List<Community> _communities = [];
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
@override
void initState() {
super.initState();
_searchController.text = context
.read<UiViewStateService>()
.channelsSearchText;
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MeshCoreConnector>().getChannels();
_loadCommunities();
@@ -61,6 +65,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
Future<void> _loadCommunities() async {
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
final communities = await _communityStore.loadCommunities();
if (mounted) {
setState(() {
@@ -106,7 +112,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final viewState = context.watch<UiViewStateService>();
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
@@ -199,6 +208,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final filteredChannels = _filterAndSortChannels(
channels,
connector,
viewState,
);
return Column(
@@ -213,17 +223,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_searchQuery.isNotEmpty)
if (viewState.channelsSearchText.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchDebounce?.cancel();
_searchDebounce = null;
_searchController.clear();
setState(() {
_searchQuery = '';
});
context
.read<UiViewStateService>()
.setChannelsSearchText('');
},
),
_buildFilterButton(),
_buildFilterButton(viewState),
],
),
border: OutlineInputBorder(
@@ -240,9 +252,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
const Duration(milliseconds: 300),
() {
if (!mounted) return;
setState(() {
_searchQuery = value.toLowerCase();
});
context
.read<UiViewStateService>()
.setChannelsSearchText(value);
},
);
},
@@ -277,8 +289,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
],
)
: (_sortOption == ChannelSortOption.manual &&
_searchQuery.isEmpty)
: (viewState.channelsSortOption ==
ChannelSortOption.manual &&
viewState.channelsSearchText.isEmpty)
? ReorderableListView.builder(
padding: const EdgeInsets.only(
left: 16,
@@ -406,78 +419,102 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return Card(
key: ValueKey('channel_${channel.index}'),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
dense: true,
minVerticalPadding: 0,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
visualDensity: const VisualDensity(vertical: -2),
leading: Stack(
children: [
CircleAvatar(
backgroundColor: bgColor,
child: Icon(icon, color: iconColor),
),
if (isCommunityChannel)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).cardColor,
width: 2,
child: GestureDetector(
onSecondaryTapUp: PlatformInfo.isDesktop
? (_) => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
)
: null,
child: ListTile(
dense: true,
minVerticalPadding: 0,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
visualDensity: const VisualDensity(vertical: -2),
leading: Stack(
children: [
CircleAvatar(
backgroundColor: bgColor,
child: Icon(icon, color: iconColor),
),
if (isCommunityChannel)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).cardColor,
width: 2,
),
),
child: const Icon(
Icons.people,
size: 8,
color: Colors.white,
),
),
child: const Icon(Icons.people, size: 8, color: Colors.white),
),
),
],
),
title: Text(
channel.name.isEmpty
? context.l10n.channels_channelIndex(channel.index)
: channel.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(width: 4),
],
if (showDragHandle && dragIndex != null)
ReorderableDelayedDragStartListener(
index: dragIndex,
child: Icon(
Icons.drag_handle,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
title: Text(
channel.name.isEmpty
? context.l10n.channels_channelIndex(channel.index)
: channel.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(width: 4),
],
if (showDragHandle && dragIndex != null)
ReorderableDelayedDragStartListener(
index: dragIndex,
child: Icon(
Icons.drag_handle,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
onTap: () async {
connector.markChannelRead(channel.index);
await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelChatScreen(channel: channel),
),
],
),
onTap: () async {
final unread = connector.getUnreadCountForChannelIndex(
channel.index,
);
}
},
onLongPress: () => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
connector.markChannelRead(channel.index);
await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelChatScreen(
channel: channel,
initialUnreadCount: unread,
),
),
);
}
},
onLongPress: () => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
),
),
),
);
@@ -578,59 +615,40 @@ class _ChannelsScreenState extends State<ChannelsScreen>
await showDisconnectDialog(context, connector);
}
Widget _buildFilterButton() {
const actionSortManual = 0;
const actionSortName = 1;
const actionSortLatest = 2;
const actionSortUnread = 3;
return SortFilterMenu(
Widget _buildFilterButton(UiViewStateService viewState) {
return SortFilterMenu<ChannelSortOption>(
tooltip: context.l10n.listFilter_tooltip,
sections: [
SortFilterMenuSection(
SortFilterMenuSection<ChannelSortOption>(
title: context.l10n.channels_sortBy,
options: [
SortFilterMenuOption(
value: actionSortManual,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.manual,
label: context.l10n.channels_sortManual,
checked: _sortOption == ChannelSortOption.manual,
checked: viewState.channelsSortOption == ChannelSortOption.manual,
),
SortFilterMenuOption(
value: actionSortName,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.name,
label: context.l10n.channels_sortAZ,
checked: _sortOption == ChannelSortOption.name,
checked: viewState.channelsSortOption == ChannelSortOption.name,
),
SortFilterMenuOption(
value: actionSortLatest,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.latestMessages,
label: context.l10n.channels_sortLatestMessages,
checked: _sortOption == ChannelSortOption.latestMessages,
checked:
viewState.channelsSortOption ==
ChannelSortOption.latestMessages,
),
SortFilterMenuOption(
value: actionSortUnread,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.unread,
label: context.l10n.channels_sortUnread,
checked: _sortOption == ChannelSortOption.unread,
checked: viewState.channelsSortOption == ChannelSortOption.unread,
),
],
),
],
onSelected: (action) {
setState(() {
switch (action) {
case actionSortManual:
_sortOption = ChannelSortOption.manual;
break;
case actionSortLatest:
_sortOption = ChannelSortOption.latestMessages;
break;
case actionSortUnread:
_sortOption = ChannelSortOption.unread;
break;
case actionSortName:
default:
_sortOption = ChannelSortOption.name;
break;
}
});
onSelected: (sortOption) {
viewState.setChannelsSortOption(sortOption);
},
);
}
@@ -638,11 +656,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
List<Channel> _filterAndSortChannels(
List<Channel> channels,
MeshCoreConnector connector,
UiViewStateService viewState,
) {
var filtered = channels.where((channel) {
if (_searchQuery.isEmpty) return true;
if (viewState.channelsSearchText.isEmpty) return true;
final label = _normalizeChannelName(channel);
return label.toLowerCase().contains(_searchQuery);
return label.toLowerCase().contains(
viewState.channelsSearchText.toLowerCase(),
);
}).toList();
int compareByName(Channel a, Channel b) {
@@ -651,7 +672,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
}
switch (_sortOption) {
switch (viewState.channelsSortOption) {
case ChannelSortOption.manual:
break;
case ChannelSortOption.latestMessages:
@@ -712,6 +733,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
bool isRegularHashtag = true;
Community? selectedCommunity;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
@@ -763,7 +786,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
);
}
Widget? buildExpandedContent() {
Widget? buildExpandedContent(
ChannelMessageStore channelMessageStore,
) {
switch (selectedOption) {
case 0: // Create Private Channel
return Column(
@@ -788,18 +813,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
children: [
Expanded(
child: FilledButton(
onPressed: () {
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;
@@ -810,15 +832,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
psk[i] = random.nextInt(256);
}
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk);
await connector.setChannel(
nextIndex,
name,
psk,
);
await channelMessageStore.clearChannelMessages(
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),
),
);
}
@@ -872,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;
@@ -889,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;
@@ -905,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),
),
);
}
@@ -942,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,
),
);
}
@@ -1072,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;
@@ -1100,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;
@@ -1134,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,
),
),
);
@@ -1234,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;
@@ -1276,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),
),
);
@@ -1329,7 +1334,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_createPrivateChannelDesc,
),
if (selectedOption == 0) buildExpandedContent()!,
if (selectedOption == 0)
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 1,
@@ -1338,7 +1344,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinPrivateChannelDesc,
),
if (selectedOption == 1) buildExpandedContent()!,
if (selectedOption == 1)
buildExpandedContent(_channelMessageStore)!,
if (!hasPublicChannel) ...[
const Divider(height: 1),
buildOptionTile(
@@ -1348,7 +1355,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinPublicChannelDesc,
),
if (selectedOption == 2) buildExpandedContent()!,
if (selectedOption == 2)
buildExpandedContent(_channelMessageStore)!,
],
const Divider(height: 1),
buildOptionTile(
@@ -1358,7 +1366,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinHashtagChannelDesc,
),
if (selectedOption == 3) buildExpandedContent()!,
if (selectedOption == 3)
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 4,
@@ -1366,7 +1375,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_scanQr,
subtitle: dialogContext.l10n.community_join,
),
if (selectedOption == 4) buildExpandedContent()!,
if (selectedOption == 4)
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 5,
@@ -1374,7 +1384,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_create,
subtitle: dialogContext.l10n.community_createDesc,
),
if (selectedOption == 5) buildExpandedContent()!,
if (selectedOption == 5)
buildExpandedContent(_channelMessageStore)!,
],
),
),
@@ -1463,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;
}
@@ -1479,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'),
),
);
}
},
@@ -1524,25 +1536,23 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try {
await connector.deleteChannel(channel.index);
channelMessageStore.clearChannelMessages(channel.index);
await channelMessageStore.clearChannelMessages(channel.index);
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),
),
);
@@ -1563,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),
);
}
@@ -1749,6 +1760,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
final channelCount = communityChannels.length;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog(
context: context,
@@ -1778,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)),
);
}
},
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
class ChromeRequiredScreen extends StatelessWidget {
const ChromeRequiredScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.browser_not_supported_rounded,
size: 80,
color: Colors.orange,
),
),
const SizedBox(height: 32),
Text(
l10n.scanner_chromeRequired,
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 16),
Text(
l10n.scanner_chromeRequiredMessage,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.white70 : Colors.black54,
height: 1.5,
),
),
const SizedBox(height: 48),
// We can't really "fix" it for them other than telling them to use Chrome
// but we can provide a nice visual.
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
const SizedBox(width: 12),
Text(
"Web Bluetooth requires a Chromium browser",
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
);
}
}
+19 -16
View File
@@ -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.
///
@@ -51,6 +52,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
_isProcessing = true;
});
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
@@ -73,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 {
@@ -90,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),
);
}
@@ -209,6 +211,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
bool addPublicChannel,
) async {
// Save community to local storage
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
@@ -224,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
@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/models/companion_radio_stats.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:provider/provider.dart';
class CompanionRadioStatsScreen extends StatefulWidget {
const CompanionRadioStatsScreen({super.key});
@override
State<CompanionRadioStatsScreen> createState() =>
_CompanionRadioStatsScreenState();
}
class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
final List<double> _noiseHistory = [];
static const int _maxSamples = 120;
MeshCoreConnector? _connector;
DateTime? _lastChartSampleAt;
@override
void initState() {
super.initState();
final c = context.read<MeshCoreConnector>();
_connector = c;
c.acquireRadioStatsPolling();
c.setPollingInterval(1);
c.radioStatsNotifier.addListener(_onStatsUpdate);
}
void _onStatsUpdate() {
final s = _connector?.radioStatsNotifier.value;
if (s == null || !mounted) return;
if (_lastChartSampleAt == s.receivedAt) return;
_lastChartSampleAt = s.receivedAt;
setState(() {
_noiseHistory.add(s.noiseFloorDbm.toDouble());
while (_noiseHistory.length > _maxSamples) {
_noiseHistory.removeAt(0);
}
});
}
@override
void dispose() {
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
_connector?.releaseRadioStatsPolling();
_connector?.setPollingInterval(30);
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.radioStats_screenTitle),
centerTitle: true,
),
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>(
selector: (_, c) => (
connected: c.isConnected,
supported: c.supportsCompanionRadioStats,
),
builder: (context, state, _) {
if (!state.connected) {
return Center(child: Text(l10n.radioStats_notConnected));
}
if (!state.supported) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
l10n.radioStats_firmwareTooOld,
textAlign: TextAlign.center,
),
),
);
}
final connector = context.read<MeshCoreConnector>();
final scheme = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return ValueListenableBuilder<CompanionRadioStats?>(
valueListenable: connector.radioStatsNotifier,
builder: (context, stats, _) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (stats != null) ...[
Text(
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
style: tt.titleMedium,
),
const SizedBox(height: 4),
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
Text(
l10n.radioStats_lastSnr(
stats.lastSnrDb.toStringAsFixed(1),
),
),
Text(l10n.radioStats_txAir(stats.txAirSecs)),
Text(l10n.radioStats_rxAir(stats.rxAirSecs)),
const SizedBox(height: 16),
] else
Text(l10n.radioStats_waiting),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: CustomPaint(
painter: _NoiseChartPainter(
samples: List<double>.from(_noiseHistory),
colorScheme: scheme,
textTheme: tt,
),
child: const SizedBox.expand(),
),
),
const SizedBox(height: 8),
Text(
l10n.radioStats_chartCaption,
style: tt.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
);
},
);
},
),
);
}
}
class _NoiseChartPainter extends CustomPainter {
final List<double> samples;
final ColorScheme colorScheme;
final TextTheme textTheme;
_NoiseChartPainter({
required this.samples,
required this.colorScheme,
required this.textTheme,
});
@override
void paint(Canvas canvas, Size size) {
final bg = Paint()..color = colorScheme.surfaceContainerHighest;
final border = Paint()
..color = colorScheme.outlineVariant
..style = PaintingStyle.stroke
..strokeWidth = 1;
final grid = Paint()
..color = colorScheme.outlineVariant.withValues(alpha: 0.5)
..strokeWidth = 1;
final line = Paint()
..color = colorScheme.primary
..strokeWidth = 2
..style = PaintingStyle.stroke;
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
bg,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
border,
);
const padL = 40.0;
const padR = 8.0;
const padT = 8.0;
const padB = 24.0;
final chart = Rect.fromLTRB(
padL,
padT,
size.width - padR,
size.height - padB,
);
for (var i = 0; i <= 4; i++) {
final y = chart.top + (chart.height * i / 4);
canvas.drawLine(Offset(chart.left, y), Offset(chart.right, y), grid);
}
if (samples.length < 2) {
final tp = TextPainter(
text: TextSpan(
text: '',
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(
canvas,
Offset(chart.left + 4, chart.top + chart.height / 2 - tp.height / 2),
);
return;
}
double minV = samples.reduce((a, b) => a < b ? a : b);
double maxV = samples.reduce((a, b) => a > b ? a : b);
if ((maxV - minV).abs() < 1) {
minV -= 2;
maxV += 2;
}
final span = maxV - minV;
for (var i = 0; i <= 2; i++) {
final v = maxV - span * i / 2;
final tp = _yAxisLabel(v);
final y = chart.top + (chart.height * i / 2) - tp.height / 2;
tp.paint(canvas, Offset(4, y));
}
final path = Path();
for (var i = 0; i < samples.length; i++) {
final x = chart.left + (chart.width * i / (samples.length - 1));
final t = (samples[i] - minV) / span;
final y = chart.bottom - t * chart.height;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, line);
}
@override
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
return oldDelegate.samples.length != samples.length ||
oldDelegate.colorScheme != colorScheme;
}
TextPainter _yAxisLabel(double v) {
final tp = TextPainter(
text: TextSpan(
text: v.round().toString(),
style: textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
textDirection: TextDirection.ltr,
)..layout();
return tp;
}
}
File diff suppressed because it is too large Load Diff
-280
View File
@@ -1,280 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/quick_switch_bar.dart';
import 'channels_screen.dart';
import 'contacts_screen.dart';
import 'map_screen.dart';
import 'settings_screen.dart';
/// Main hub screen after connecting to a MeshCore device
class DeviceScreen extends StatefulWidget {
const DeviceScreen({super.key});
@override
State<DeviceScreen> createState() => _DeviceScreenState();
}
class _DeviceScreenState extends State<DeviceScreen>
with DisconnectNavigationMixin {
bool _showBatteryVoltage = false;
int _quickIndex = 0;
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
}
final theme = Theme.of(context);
return PopScope(
canPop: false,
child: Scaffold(
appBar: AppBar(
leading: _buildBatteryIndicator(connector, context),
titleSpacing: 16,
centerTitle: false,
title: _buildAppBarTitle(connector, theme),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(Icons.bluetooth_disabled),
tooltip: context.l10n.common_disconnect,
onPressed: () => _disconnect(context, connector),
),
IconButton(
icon: const Icon(Icons.tune),
tooltip: context.l10n.common_settings,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
),
),
],
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
children: [
_buildConnectionCard(connector, context),
const SizedBox(height: 16),
_buildSectionLabel(theme, context.l10n.device_quickSwitch),
const SizedBox(height: 12),
_buildQuickSwitchBar(context),
],
),
),
),
);
},
);
}
Widget _buildAppBarTitle(MeshCoreConnector connector, ThemeData theme) {
final colorScheme = theme.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.device_meshcore,
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.8,
color: colorScheme.onSurfaceVariant,
),
),
Text(
connector.deviceDisplayName,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
);
}
Widget _buildSectionLabel(ThemeData theme, String text) {
return Text(
text,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.6,
color: theme.colorScheme.onSurfaceVariant,
),
);
}
Widget _buildConnectionCard(
MeshCoreConnector connector,
BuildContext context,
) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Card(
elevation: 0,
color: colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 24,
backgroundColor: colorScheme.primaryContainer,
child: Icon(
Icons.wifi_tethering_rounded,
color: colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
connector.deviceDisplayName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
connector.deviceIdLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Chip(
avatar: Icon(
Icons.check_circle,
size: 18,
color: colorScheme.onSecondaryContainer,
),
label: Text(context.l10n.common_connected),
backgroundColor: colorScheme.secondaryContainer,
labelStyle: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
visualDensity: VisualDensity.compact,
),
_buildBatteryIndicator(connector, context),
],
),
],
),
),
);
}
Widget _buildQuickSwitchBar(BuildContext context) {
return QuickSwitchBar(
selectedIndex: _quickIndex,
onDestinationSelected: (index) {
_openQuickDestination(index, context);
},
);
}
Widget _buildBatteryIndicator(
MeshCoreConnector connector,
BuildContext context,
) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts;
final percentLabel = percent != null ? '$percent%' : '--%';
final voltageLabel = millivolts == null
? '-- V'
: '${(millivolts / 1000.0).toStringAsFixed(2)} V';
final displayLabel = _showBatteryVoltage ? voltageLabel : percentLabel;
final icon = _batteryIcon(percent);
return ActionChip(
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
label: Text(displayLabel),
labelStyle: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
backgroundColor: colorScheme.secondaryContainer,
visualDensity: VisualDensity.compact,
onPressed: () {
setState(() {
_showBatteryVoltage = !_showBatteryVoltage;
});
},
);
}
IconData _batteryIcon(int? percent) {
if (percent == null) return Icons.battery_unknown;
if (percent <= 15) return Icons.battery_alert;
return Icons.battery_full;
}
void _openQuickDestination(int index, BuildContext context) {
if (_quickIndex != index) {
setState(() {
_quickIndex = index;
});
}
switch (index) {
case 0:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
);
break;
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
}
Future<void> _disconnect(
BuildContext context,
MeshCoreConnector connector,
) async {
await showDisconnectDialog(context, connector);
}
}

Some files were not shown because too many files have changed in this diff Show More