Compare commits

...

111 Commits

Author SHA1 Message Date
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
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 ebbc367fec Merge pull request #310 from zjs81/main
merge dev
2026-03-23 18:46:40 -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
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
118 changed files with 29868 additions and 1674 deletions
+3
View File
@@ -33,6 +33,9 @@ migrate_working_dir/
pubspec.lock
/build/
/coverage/
# fvm project files
.fvm/
.fvmrc
# Symbolication related
app.*.symbols
+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.
+11 -2
View File
@@ -150,7 +150,8 @@ lib/
├── main.dart # App entry point
├── connector/
│ ├── meshcore_connector.dart # BLE communication & state management
── meshcore_protocol.dart # Protocol definitions & frame parsing
── meshcore_protocol.dart # Protocol definitions & frame parsing
│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!)
├── screens/
│ ├── scanner_screen.dart # Device scanning (home screen)
│ ├── contacts_screen.dart # Contact list
@@ -184,7 +185,15 @@ lib/
### Device Discovery
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently:
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
### Message Format
+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
+6 -1
View File
@@ -49,7 +49,12 @@ enum MeshCoreConnectionState {
## BLE Connection Lifecycle
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
2. **Connect** with 15-second timeout
3. **Request MTU** 185 bytes (non-web only)
4. **Discover services** and locate NUS
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -202,13 +202,15 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdSendAnonReq = 57;
const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdGetStats = 56;
const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
const int cmdSetPathHashMode = 61;
// Text message types
const int txtTypePlain = 0;
@@ -245,6 +247,11 @@ const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
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;
@@ -554,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();
+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_",
];
}
+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';
}
}
+19 -17
View File
@@ -3,8 +3,17 @@ 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,
@@ -12,14 +21,9 @@ class LinkHandler {
required TextStyle style,
TextStyle? linkStyle,
}) {
final effectiveLinkStyle =
linkStyle ??
style.copyWith(
color: Colors.green,
decoration: TextDecoration.underline,
);
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
const linkifiers = [UrlLinkifier()];
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) {
@@ -90,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,
);
}
}
+5
View File
@@ -109,4 +109,9 @@ class ReactionHelper {
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
}
/// Encode a reaction message that parseReaction() can parse.
static String encodeReaction(String hash, String emojiIndex) {
return 'r:$hash:$emojiIndex';
}
}
+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) {
+133 -10
View File
@@ -1922,13 +1922,6 @@
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
"contact_teleLoc": "Местоположение на телеметрията",
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
@@ -1940,8 +1933,138 @@
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Мулти-потвърди: {value}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
"map_showOverlaps": "Покриване на ключа на повтаряча",
"map_runTraceWithReturnPath": "Върни се по същия път."
}
"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": "Инструменти за гости",
"settings_multiAck": "Множество потвърждения"
}
+133 -10
View File
@@ -1950,13 +1950,6 @@
"contact_lastSeen": "Zuletzt gesehen",
"contact_clearChat": "Chat löschen",
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
"appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
"appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
@@ -1969,7 +1962,137 @@
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
"map_showOverlaps": "Überlappungen der Repeater-Taste",
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren."
}
"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",
"settings_multiAck": "Mehrere Bestätigungen"
}
+151 -14
View File
@@ -127,6 +127,7 @@
}
}
},
"scanner_stop": "Stop",
"scanner_scan": "Scan",
"scanner_bluetoothOff": "Bluetooth is off",
@@ -177,14 +178,7 @@
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.",
"settings_multiAck": "Multi-ACKs: {value}",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_multiAck": "Multi-ACKs",
"settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions",
"settings_sendAdvertisement": "Send Advertisement",
@@ -302,8 +296,12 @@
"path_routeWeight": "{weight}/{max}",
"@path_routeWeight": {
"placeholders": {
"weight": { "type": "String" },
"max": { "type": "String" }
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"appSettings_battery": "Battery",
@@ -602,6 +600,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}",
@@ -1024,8 +1031,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)",
@@ -1091,7 +1098,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",
@@ -1102,6 +1112,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)",
@@ -1332,6 +1350,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.",
@@ -1977,5 +1997,122 @@
"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?"
}
"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"
}
+133 -10
View File
@@ -1950,13 +1950,6 @@
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
"contact_teleEnv": "Entorno de Telemetría",
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Peso inicial de la ruta",
"appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
@@ -1969,7 +1962,137 @@
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Superposiciones de tecla repetidora",
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino."
}
"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",
"settings_multiAck": "Múltiples respuestas de confirmación"
}
+156 -33
View File
@@ -143,8 +143,8 @@
"settings_frequencyHelper": "300,0 - 2 500,0",
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
"settings_bandwidth": "Bande passante",
"settings_spreadingFactor": "Facteur de répartition",
"settings_codingRate": "Taux de codage",
"settings_spreadingFactor": "Facteur de répartition (SF)",
"settings_codingRate": "Taux de codage (CR)",
"settings_txPower": "TX Puissance (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
@@ -567,7 +567,7 @@
"chat_clearPath": "Effacer le chemin",
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
"chat_floodModeSubtitle": "Désactive l'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d'application pour rebasculer en mode auto par la suite.",
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
"chat_fullPath": "Chemin complet",
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
@@ -643,7 +643,7 @@
},
"map_chat": "Chat",
"map_repeater": "Répéteur",
"map_room": "Salle",
"map_room": "Room Server",
"map_sensor": "Capteur",
"map_pinDm": "Clé (DM)",
"map_pinPrivate": "Verrouiller (Privé)",
@@ -682,7 +682,7 @@
"map_showSharedMarkers": "Afficher les marqueurs partagés",
"map_lastSeenTime": "Dernière fois vu",
"map_sharedPin": "Clé partagée",
"map_joinRoom": "Rejoindre la salle",
"map_joinRoom": "Rejoindre le room server",
"map_manageRepeater": "Gérer le répéteur",
"mapCache_title": "Cache de Carte Hors Ligne",
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
@@ -865,7 +865,7 @@
"path_labelHexPrefixes": "Préfixes hexadécimaux",
"path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)",
"path_selectFromContacts": "Sélectionner à partir des contacts :",
"path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.",
"path_noRepeatersFound": "Aucun répéteur ou room server n'a été trouvé.",
"path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.",
"path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}",
"@path_invalidHexPrefixes": {
@@ -996,15 +996,15 @@
"repeater_txPower": "TX Puissance",
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Bande passante",
"repeater_spreadingFactor": "Facteur de répartition",
"repeater_codingRate": "Taux de codage",
"repeater_spreadingFactor": "Facteur de répartition (SF)",
"repeater_codingRate": "Taux de codage (CR)",
"repeater_locationSettings": "Paramètres de localisation",
"repeater_latitude": "Latitude",
"repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)",
"repeater_longitude": "Longitude",
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
"repeater_features": "Fonctionnalités",
"repeater_packetForwarding": "Transfert de paquets",
"repeater_packetForwarding": "Mode répéteur",
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
"repeater_guestAccess": "Accès Invité",
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
@@ -1377,7 +1377,7 @@
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCode": "Scanner un QR code",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #equipe",
@@ -1466,8 +1466,8 @@
"community_join": "Rejoindre",
"community_joinTitle": "Rejoindre la communauté",
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
"community_scanQr": "Scanner la communauté QR",
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
"community_scanQr": "Scanner un QR code de communauté",
"community_scanInstructions": "Pointez l'appareil photo vers un QR code de communauté.",
"community_showQr": "Afficher le QR Code",
"community_publicChannel": "Communauté Publique",
"community_hashtagChannel": "Hashtag Communauté",
@@ -1478,13 +1478,13 @@
"community_qrTitle": "Partager Communauté",
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
"community_invalidQrCode": "Code QR de communauté non valide",
"community_invalidQrCode": "QR code de communauté non valide",
"community_alreadyMember": "Déjà membre",
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
"community_scanOrCreate": "Scanner un QR code ou créer une communauté pour commencer",
"community_manageCommunities": "Gérer les Communautés",
"community_delete": "Quitter la communauté",
"community_deleteConfirm": "Quitter \"{name}\" ?",
@@ -1534,10 +1534,10 @@
}
},
"community_regenerateSecret": "Régénérer le secret",
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.",
"community_regenerate": "Régénérer",
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
"community_scanToUpdateSecret": "Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"{name}\"",
"community_updateSecret": "Mettre à jour le secret",
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
"@contacts_pathTraceTo": {
@@ -1554,11 +1554,11 @@
"contacts_pathTrace": "Traçage de chemin",
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
"contacts_repeaterPing": "Pinguer le répéteur",
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
"contacts_roomPathTrace": "Traçage du chemin vers le room server",
"contacts_chatTraceRoute": "Tracer le chemin",
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
"contacts_ping": "Ping",
"contacts_roomPing": "Pinguer le serveur de la salle",
"contacts_roomPing": "Pinguer le room server",
"contacts_invalidAdvertFormat": "Données de contact non valides",
"appSettings_languageUk": "Ukrainien",
"appSettings_languageRu": "Russe",
@@ -1583,12 +1583,12 @@
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
"notification_receivedNewMessage": "Nouveau message reçu",
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
"settings_gpxExportRepeaters": "Exporter les répéteurs / room servers au format GPX",
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
"settings_gpxExportRepeatersRoom": "Emplacements des répéteurs et room servers",
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
@@ -1800,15 +1800,15 @@
"contacts_unread": "Non lu",
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
"contacts_searchRoomServers": "Rechercher {number}{str} room server...",
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
"settings_contactSettings": "Paramètres de contact",
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les room servers",
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les room servers découverts",
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
"contactsSettings_title": "Paramètres des contacts",
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Dernière fois vu",
"contact_clearChat": "Effacer la conversation",
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.",
"appSettings_initialRouteWeight": "Poids initial de l'itinéraire",
"appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
@@ -1940,8 +1933,138 @@
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Multi-ACKs : {value}",
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
"map_showOverlaps": "Chevauchement de la touche répétitive",
"map_runTraceWithReturnPath": "Revenir sur le même chemin."
}
"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",
"settings_multiAck": "Plusieurs accusés de réception"
}
+2108
View File
File diff suppressed because it is too large Load Diff
+133 -10
View File
@@ -1922,13 +1922,6 @@
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
"contact_teleEnv": "Ambiente di telemetria",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Peso iniziale del percorso",
"appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
"appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
@@ -1941,7 +1934,137 @@
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso"
}
"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",
"settings_multiAck": "ACK multipli"
}
+2108
View File
File diff suppressed because it is too large Load Diff
+2108
View File
File diff suppressed because it is too large Load Diff
+391 -10
View File
@@ -10,7 +10,10 @@ import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_hu.dart';
import 'app_localizations_it.dart';
import 'app_localizations_ja.dart';
import 'app_localizations_ko.dart';
import 'app_localizations_nl.dart';
import 'app_localizations_pl.dart';
import 'app_localizations_pt.dart';
@@ -112,7 +115,10 @@ abstract class AppLocalizations {
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('hu'),
Locale('it'),
Locale('ja'),
Locale('ko'),
Locale('nl'),
Locale('pl'),
Locale('pt'),
@@ -895,8 +901,8 @@ abstract class AppLocalizations {
/// No description provided for @settings_multiAck.
///
/// In en, this message translates to:
/// **'Multi-ACKs: {value}'**
String settings_multiAck(String value);
/// **'Multi-ACKs'**
String get settings_multiAck;
/// No description provided for @settings_telemetryModeUpdated.
///
@@ -2290,6 +2296,18 @@ abstract class AppLocalizations {
/// **'No messages yet'**
String get chat_noMessages;
/// No description provided for @chat_sendMessage.
///
/// In en, this message translates to:
/// **'Send message'**
String get chat_sendMessage;
/// No description provided for @chat_sendMessageTo.
///
/// In en, this message translates to:
/// **'Send a message to {contactName}'**
String chat_sendMessageTo(String contactName);
/// No description provided for @chat_sendMessageToStart.
///
/// In en, this message translates to:
@@ -2320,12 +2338,6 @@ abstract class AppLocalizations {
/// **'Location'**
String get chat_location;
/// No description provided for @chat_sendMessageTo.
///
/// In en, this message translates to:
/// **'Send a message to {contactName}'**
String chat_sendMessageTo(String contactName);
/// No description provided for @chat_typeMessage.
///
/// In en, this message translates to:
@@ -3426,13 +3438,13 @@ abstract class AppLocalizations {
/// No description provided for @login_repeaterDescription.
///
/// In en, this message translates to:
/// **'Enter the repeater password to access settings and status.'**
/// **'Enter the repeater password for guest or admin access.'**
String get login_repeaterDescription;
/// No description provided for @login_roomDescription.
///
/// In en, this message translates to:
/// **'Enter the room password to access settings and status.'**
/// **'Enter the room password for guest or admin access.'**
String get login_roomDescription;
/// No description provided for @login_routing.
@@ -3597,12 +3609,30 @@ abstract class AppLocalizations {
/// **'Room Server Management'**
String get room_management;
/// No description provided for @repeater_guest.
///
/// In en, this message translates to:
/// **'Repeater Information'**
String get repeater_guest;
/// No description provided for @room_guest.
///
/// In en, this message translates to:
/// **'Room Server Information'**
String get room_guest;
/// No description provided for @repeater_managementTools.
///
/// In en, this message translates to:
/// **'Management Tools'**
String get repeater_managementTools;
/// No description provided for @repeater_guestTools.
///
/// In en, this message translates to:
/// **'Guest Tools'**
String get repeater_guestTools;
/// No description provided for @repeater_status.
///
/// In en, this message translates to:
@@ -3663,6 +3693,18 @@ abstract class AppLocalizations {
/// **'Configure repeater parameters'**
String get repeater_settingsSubtitle;
/// Repeater setting: auto sync device clock after successful login
///
/// In en, this message translates to:
/// **'Clock sync after login'**
String get repeater_clockSyncAfterLogin;
/// Repeater setting subtitle: describes the clock sync after login behavior
///
/// In en, this message translates to:
/// **'Automatically send \"clock sync\" after a successful login'**
String get repeater_clockSyncAfterLoginSubtitle;
/// No description provided for @repeater_statusTitle.
///
/// In en, this message translates to:
@@ -4316,6 +4358,18 @@ abstract class AppLocalizations {
/// **'Clock'**
String get repeater_cliQuickClock;
/// No description provided for @repeater_cliQuickClockSync.
///
/// In en, this message translates to:
/// **'Clock Sync'**
String get repeater_cliQuickClockSync;
/// No description provided for @repeater_cliQuickDiscovery.
///
/// In en, this message translates to:
/// **'Discover Neighbors'**
String get repeater_cliQuickDiscovery;
/// No description provided for @repeater_cliHelpAdvert.
///
/// In en, this message translates to:
@@ -6016,6 +6070,324 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Are you sure you want to delete all discovered contacts?'**
String get discoveredContacts_deleteContactAllContent;
/// No description provided for @chat_sendCooldown.
///
/// In en, this message translates to:
/// **'Please wait a moment before sending again.'**
String get chat_sendCooldown;
/// No description provided for @appSettings_jumpToOldestUnread.
///
/// In en, this message translates to:
/// **'Jump to oldest unread'**
String get appSettings_jumpToOldestUnread;
/// No description provided for @appSettings_jumpToOldestUnreadSubtitle.
///
/// In en, this message translates to:
/// **'When opening a chat with unread messages, scroll to the first unread instead of the latest.'**
String get appSettings_jumpToOldestUnreadSubtitle;
/// No description provided for @appSettings_languageHu.
///
/// In en, this message translates to:
/// **'Hungarian'**
String get appSettings_languageHu;
/// No description provided for @appSettings_languageJa.
///
/// In en, this message translates to:
/// **'Japanese'**
String get appSettings_languageJa;
/// No description provided for @appSettings_languageKo.
///
/// In en, this message translates to:
/// **'Korean'**
String get appSettings_languageKo;
/// No description provided for @radioStats_tooltip.
///
/// In en, this message translates to:
/// **'Radio & mesh stats'**
String get radioStats_tooltip;
/// No description provided for @radioStats_screenTitle.
///
/// In en, this message translates to:
/// **'Radio stats'**
String get radioStats_screenTitle;
/// No description provided for @radioStats_notConnected.
///
/// In en, this message translates to:
/// **'Connect to a device to view radio statistics.'**
String get radioStats_notConnected;
/// No description provided for @radioStats_firmwareTooOld.
///
/// In en, this message translates to:
/// **'Radio statistics require companion firmware v8 or newer.'**
String get radioStats_firmwareTooOld;
/// No description provided for @radioStats_waiting.
///
/// In en, this message translates to:
/// **'Waiting for data…'**
String get radioStats_waiting;
/// No description provided for @radioStats_noiseFloor.
///
/// In en, this message translates to:
/// **'Noise floor: {noiseDbm} dBm'**
String radioStats_noiseFloor(int noiseDbm);
/// No description provided for @radioStats_lastRssi.
///
/// In en, this message translates to:
/// **'Last RSSI: {rssiDbm} dBm'**
String radioStats_lastRssi(int rssiDbm);
/// No description provided for @radioStats_lastSnr.
///
/// In en, this message translates to:
/// **'Last SNR: {snr} dB'**
String radioStats_lastSnr(String snr);
/// No description provided for @radioStats_txAir.
///
/// In en, this message translates to:
/// **'TX airtime (total): {seconds} s'**
String radioStats_txAir(int seconds);
/// No description provided for @radioStats_rxAir.
///
/// In en, this message translates to:
/// **'RX airtime (total): {seconds} s'**
String radioStats_rxAir(int seconds);
/// No description provided for @radioStats_chartCaption.
///
/// In en, this message translates to:
/// **'Noise floor (dBm) over recent samples.'**
String get radioStats_chartCaption;
/// No description provided for @radioStats_stripNoise.
///
/// In en, this message translates to:
/// **'Noise floor: {noiseDbm} dBm'**
String radioStats_stripNoise(int noiseDbm);
/// No description provided for @radioStats_stripWaiting.
///
/// In en, this message translates to:
/// **'Fetching radio stats…'**
String get radioStats_stripWaiting;
/// No description provided for @radioStats_settingsTile.
///
/// In en, this message translates to:
/// **'Radio stats'**
String get radioStats_settingsTile;
/// No description provided for @radioStats_settingsSubtitle.
///
/// In en, this message translates to:
/// **'Noise floor, RSSI, SNR, and airtime'**
String get radioStats_settingsSubtitle;
/// No description provided for @translation_title.
///
/// In en, this message translates to:
/// **'Translation'**
String get translation_title;
/// No description provided for @translation_enableTitle.
///
/// In en, this message translates to:
/// **'Enable translation'**
String get translation_enableTitle;
/// No description provided for @translation_enableSubtitle.
///
/// In en, this message translates to:
/// **'Translate incoming messages and allow pre-send translation.'**
String get translation_enableSubtitle;
/// No description provided for @translation_composerTitle.
///
/// In en, this message translates to:
/// **'Translate before sending'**
String get translation_composerTitle;
/// No description provided for @translation_composerSubtitle.
///
/// In en, this message translates to:
/// **'Controls the default state of the composer translation icon.'**
String get translation_composerSubtitle;
/// No description provided for @translation_targetLanguage.
///
/// In en, this message translates to:
/// **'Target language'**
String get translation_targetLanguage;
/// No description provided for @translation_useAppLanguage.
///
/// In en, this message translates to:
/// **'Use app language'**
String get translation_useAppLanguage;
/// No description provided for @translation_downloadedModelLabel.
///
/// In en, this message translates to:
/// **'Downloaded model'**
String get translation_downloadedModelLabel;
/// No description provided for @translation_presetModelLabel.
///
/// In en, this message translates to:
/// **'Preset Hugging Face model'**
String get translation_presetModelLabel;
/// No description provided for @translation_manualUrlLabel.
///
/// In en, this message translates to:
/// **'Manual model URL'**
String get translation_manualUrlLabel;
/// No description provided for @translation_downloadModel.
///
/// In en, this message translates to:
/// **'Download model'**
String get translation_downloadModel;
/// No description provided for @translation_downloading.
///
/// In en, this message translates to:
/// **'Downloading...'**
String get translation_downloading;
/// No description provided for @translation_working.
///
/// In en, this message translates to:
/// **'Working...'**
String get translation_working;
/// No description provided for @translation_stop.
///
/// In en, this message translates to:
/// **'Stop'**
String get translation_stop;
/// No description provided for @translation_mergingChunks.
///
/// In en, this message translates to:
/// **'Merging downloaded chunks into final file...'**
String get translation_mergingChunks;
/// No description provided for @translation_downloadedModels.
///
/// In en, this message translates to:
/// **'Downloaded models'**
String get translation_downloadedModels;
/// No description provided for @translation_deleteModel.
///
/// In en, this message translates to:
/// **'Delete model'**
String get translation_deleteModel;
/// No description provided for @translation_modelDownloaded.
///
/// In en, this message translates to:
/// **'Translation model downloaded.'**
String get translation_modelDownloaded;
/// No description provided for @translation_downloadStopped.
///
/// In en, this message translates to:
/// **'Download stopped.'**
String get translation_downloadStopped;
/// No description provided for @translation_downloadFailed.
///
/// In en, this message translates to:
/// **'Download failed: {error}'**
String translation_downloadFailed(String error);
/// No description provided for @translation_enterUrlFirst.
///
/// In en, this message translates to:
/// **'Enter a model URL first.'**
String get translation_enterUrlFirst;
/// No description provided for @scanner_linuxPairingShowPin.
///
/// In en, this message translates to:
/// **'Show PIN'**
String get scanner_linuxPairingShowPin;
/// No description provided for @scanner_linuxPairingHidePin.
///
/// In en, this message translates to:
/// **'Hide PIN'**
String get scanner_linuxPairingHidePin;
/// No description provided for @scanner_linuxPairingPinTitle.
///
/// In en, this message translates to:
/// **'Bluetooth Pairing PIN'**
String get scanner_linuxPairingPinTitle;
/// No description provided for @scanner_linuxPairingPinPrompt.
///
/// In en, this message translates to:
/// **'Enter PIN for {deviceName} (leave blank if none).'**
String scanner_linuxPairingPinPrompt(String deviceName);
/// No description provided for @translation_messageTranslation.
///
/// In en, this message translates to:
/// **'Message translation'**
String get translation_messageTranslation;
/// No description provided for @translation_translateBeforeSending.
///
/// In en, this message translates to:
/// **'Translate before sending'**
String get translation_translateBeforeSending;
/// No description provided for @translation_composerEnabledHint.
///
/// In en, this message translates to:
/// **'Messages will be translated before send.'**
String get translation_composerEnabledHint;
/// No description provided for @translation_composerDisabledHint.
///
/// In en, this message translates to:
/// **'Send messages in the original typed language.'**
String get translation_composerDisabledHint;
/// No description provided for @translation_translateTo.
///
/// In en, this message translates to:
/// **'Translate to {language}'**
String translation_translateTo(String language);
/// No description provided for @translation_translationOptions.
///
/// In en, this message translates to:
/// **'Translation options'**
String get translation_translationOptions;
/// No description provided for @translation_systemLanguage.
///
/// In en, this message translates to:
/// **'System language'**
String get translation_systemLanguage;
}
class _AppLocalizationsDelegate
@@ -6034,7 +6406,10 @@ class _AppLocalizationsDelegate
'en',
'es',
'fr',
'hu',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
@@ -6063,8 +6438,14 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'hu':
return AppLocalizationsHu();
case 'it':
return AppLocalizationsIt();
case 'ja':
return AppLocalizationsJa();
case 'ko':
return AppLocalizationsKo();
case 'nl':
return AppLocalizationsNl();
case 'pl':
+224 -8
View File
@@ -437,9 +437,7 @@ class AppLocalizationsBg extends AppLocalizations {
'Включи местоположение в обявата';
@override
String settings_multiAck(String value) {
return 'Мулти-потвърди: $value';
}
String get settings_multiAck => 'Множество потвърждения';
@override
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
@@ -1239,6 +1237,14 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_noMessages => 'Няма съобщения.';
@override
String get chat_sendMessage => 'Изпратете съобщение';
@override
String chat_sendMessageTo(String contactName) {
return 'Изпрати съобщение на $contactName';
}
@override
String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.';
@@ -1258,11 +1264,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_location => 'Местоположение';
@override
String chat_sendMessageTo(String contactName) {
return 'Изпрати съобщение на $contactName';
}
@override
String get chat_typeMessage => 'Въведете съобщение...';
@@ -2016,9 +2017,18 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_guest => 'Информация за ретранслаторите';
@override
String get room_guest => 'Информация за сървъра на стаята';
@override
String get repeater_managementTools => 'Инструменти за управление';
@override
String get repeater_guestTools => 'Инструменти за гости';
@override
String get repeater_status => 'Статус';
@@ -2053,6 +2063,14 @@ class AppLocalizationsBg extends AppLocalizations {
String get repeater_settingsSubtitle =>
'Конфигурирайте параметрите на репитера';
@override
String get repeater_clockSyncAfterLogin =>
'Синхронизиране на часовника след влизане';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.';
@override
String get repeater_statusTitle => 'Статус на повтарянето';
@@ -2429,6 +2447,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 => 'Изпраща рекламен пакет';
@@ -3484,4 +3508,196 @@ class AppLocalizationsBg extends AppLocalizations {
@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 => 'Език на системата';
}
+225 -8
View File
@@ -435,9 +435,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Ort in der Anzeige einbeziehen';
@override
String settings_multiAck(String value) {
return 'Mehrfach-Bestätigungen: $value';
}
String get settings_multiAck => 'Mehrere Bestätigungen';
@override
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
@@ -1238,6 +1236,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_noMessages => 'Noch keine Nachrichten.';
@override
String get chat_sendMessage => 'Nachricht senden';
@override
String chat_sendMessageTo(String contactName) {
return 'Sende eine Nachricht an $contactName';
}
@override
String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.';
@@ -1257,11 +1263,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...';
@@ -2014,9 +2015,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get room_management => 'Raum-Server-Verwaltung';
@override
String get repeater_guest => 'Informationen zu Repeatern';
@override
String get room_guest => 'Informationen zum Room Server';
@override
String get repeater_managementTools => 'Verwaltungs-Tools';
@override
String get repeater_guestTools => 'Gastwerkzeuge';
@override
String get repeater_status => 'Status';
@@ -2049,6 +2059,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren';
@override
String get repeater_clockSyncAfterLogin =>
'Uhrzeit-Synchronisation nach dem Anmelden';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.';
@override
String get repeater_statusTitle => 'Repeaterstatus';
@@ -2429,6 +2447,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';
@@ -3494,4 +3518,197 @@ class AppLocalizationsDe extends AppLocalizations {
@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';
}
+220 -10
View File
@@ -427,9 +427,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Include location in advert.';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
String get settings_multiAck => 'Multi-ACKs';
@override
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
@@ -1213,6 +1211,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get chat_noMessages => 'No messages yet';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Send a message to $contactName';
}
@override
String get chat_sendMessageToStart => 'Send a message to get started';
@@ -1232,11 +1238,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...';
@@ -1868,11 +1869,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get login_repeaterDescription =>
'Enter the repeater password to access settings and status.';
'Enter the repeater password for guest or admin access.';
@override
String get login_roomDescription =>
'Enter the room password to access settings and status.';
'Enter the room password for guest or admin access.';
@override
String get login_routing => 'Routing';
@@ -1976,9 +1977,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get room_management => 'Room Server Management';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override
String get repeater_managementTools => 'Management Tools';
@override
String get repeater_guestTools => 'Guest Tools';
@override
String get repeater_status => 'Status';
@@ -2011,6 +2021,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Configure repeater parameters';
@override
String get repeater_clockSyncAfterLogin => 'Clock sync after login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatically send \"clock sync\" after a successful login';
@override
String get repeater_statusTitle => 'Repeater Status';
@@ -2379,6 +2396,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';
@@ -3421,4 +3444,191 @@ class AppLocalizationsEn extends AppLocalizations {
@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';
}
+225 -8
View File
@@ -434,9 +434,7 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
String get settings_multiAck => 'Múltiples respuestas de confirmación';
@override
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
@@ -1238,6 +1236,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get chat_noMessages => 'Aún no hay mensajes';
@override
String get chat_sendMessage => 'Enviar mensaje';
@override
String chat_sendMessageTo(String contactName) {
return 'Enviar un mensaje a $contactName';
}
@override
String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar';
@@ -1257,11 +1263,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...';
@@ -2012,9 +2013,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_guest => 'Información sobre repetidores';
@override
String get room_guest => 'Información del servidor';
@override
String get repeater_managementTools => 'Herramientas de Gestión';
@override
String get repeater_guestTools => 'Herramientas para invitados';
@override
String get repeater_status => 'Estado';
@@ -2047,6 +2057,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronización del reloj después de iniciar sesión';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.';
@override
String get repeater_statusTitle => 'Estado del Repetidor';
@@ -2423,6 +2441,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';
@@ -3487,4 +3511,197 @@ class AppLocalizationsEs extends AppLocalizations {
@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';
}
+249 -32
View File
@@ -438,9 +438,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Inclure l\'emplacement dans l\'annonce';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs : $value';
}
String get settings_multiAck => 'Plusieurs accusés de réception';
@override
String get settings_telemetryModeUpdated =>
@@ -559,10 +557,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_bandwidth => 'Bande passante';
@override
String get settings_spreadingFactor => 'Facteur de répartition';
String get settings_spreadingFactor => 'Facteur de répartition (SF)';
@override
String get settings_codingRate => 'Taux de codage';
String get settings_codingRate => 'Taux de codage (CR)';
@override
String get settings_txPower => 'TX Puissance (dBm)';
@@ -946,7 +944,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String contacts_searchRoomServers(int number, String str) {
return 'Rechercher $number$str serveurs de salle...';
return 'Rechercher $number$str room server...';
}
@override
@@ -1229,7 +1227,7 @@ class AppLocalizationsFr extends AppLocalizations {
'N\'importe qui peut rejoindre les canaux #hashtag.';
@override
String get channels_scanQrCode => 'Scanner un code QR';
String get channels_scanQrCode => 'Scanner un QR code';
@override
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
@@ -1243,6 +1241,14 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_noMessages => 'Aucun message pour le moment.';
@override
String get chat_sendMessage => 'Envoyer un message';
@override
String chat_sendMessageTo(String contactName) {
return 'Envoyer un message à $contactName';
}
@override
String get chat_sendMessageToStart => 'Envoyer un message pour commencer';
@@ -1262,11 +1268,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_location => 'Emplacement';
@override
String chat_sendMessageTo(String contactName) {
return 'Envoyer un message à $contactName';
}
@override
String get chat_typeMessage => 'Saisir un message...';
@@ -1489,7 +1490,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_floodModeSubtitle =>
'Utiliser le commutateur de routage dans la barre d\'application';
'Désactive l\'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d\'application pour rebasculer en mode auto par la suite.';
@override
String get chat_floodModeEnabled =>
@@ -1614,7 +1615,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_repeater => 'Répéteur';
@override
String get map_room => 'Salle';
String get map_room => 'Room Server';
@override
String get map_sensor => 'Capteur';
@@ -1730,7 +1731,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_sharedPin => 'Clé partagée';
@override
String get map_joinRoom => 'Rejoindre la salle';
String get map_joinRoom => 'Rejoindre le room server';
@override
String get map_manageRepeater => 'Gérer le répéteur';
@@ -1999,7 +2000,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get path_noRepeatersFound =>
'Aucun répéteur ou serveur de salle n\'a été trouvé.';
'Aucun répéteur ou room server n\'a été trouvé.';
@override
String get path_customPathsRequire =>
@@ -2023,9 +2024,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get room_management => 'Administrattion Room Server';
@override
String get repeater_guest => 'Informations sur les répéteurs';
@override
String get room_guest => 'Informations sur le serveur';
@override
String get repeater_managementTools => 'Outils de Gestion';
@override
String get repeater_guestTools => 'Outils pour les invités';
@override
String get repeater_status => 'État';
@@ -2059,6 +2069,14 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_settingsSubtitle =>
'Configurer les paramètres du répéteur';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronisation de l\'horloge après la connexion';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Envoyer automatiquement une notification \"synchronisation de l\'heure\" après une connexion réussie.';
@override
String get repeater_statusTitle => 'État du répéteur';
@@ -2209,10 +2227,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_bandwidth => 'Bande passante';
@override
String get repeater_spreadingFactor => 'Facteur de répartition';
String get repeater_spreadingFactor => 'Facteur de répartition (SF)';
@override
String get repeater_codingRate => 'Taux de codage';
String get repeater_codingRate => 'Taux de codage (CR)';
@override
String get repeater_locationSettings => 'Paramètres de localisation';
@@ -2235,7 +2253,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_features => 'Fonctionnalités';
@override
String get repeater_packetForwarding => 'Transfert de paquets';
String get repeater_packetForwarding => 'Mode répéteur';
@override
String get repeater_packetForwardingSubtitle =>
@@ -2442,6 +2460,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Horloge';
@override
String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge';
@override
String get repeater_cliQuickDiscovery => 'Découvrir les voisins';
@override
String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce';
@@ -2896,11 +2920,11 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get community_scanQr => 'Scanner la communauté QR';
String get community_scanQr => 'Scanner un QR code de communauté';
@override
String get community_scanInstructions =>
'Pointez l\'appareil photo vers un code QR communautaire.';
'Pointez l\'appareil photo vers un QR code de communauté.';
@override
String get community_showQr => 'Afficher le QR Code';
@@ -2940,7 +2964,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
@override
String get community_invalidQrCode => 'Code QR de communauté non valide';
String get community_invalidQrCode => 'QR code de communauté non valide';
@override
String get community_alreadyMember => 'Déjà membre';
@@ -2964,7 +2988,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get community_scanOrCreate =>
'Scanner un code QR ou créer une communauté pour commencer';
'Scanner un QR code ou créer une communauté pour commencer';
@override
String get community_manageCommunities => 'Gérer les Communautés';
@@ -2992,7 +3016,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String community_regenerateSecretConfirm(String name) {
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.';
}
@override
@@ -3013,7 +3037,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String community_scanToUpdateSecret(String name) {
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
return 'Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"$name\"';
}
@override
@@ -3261,11 +3285,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get contacts_repeaterPing => 'Pinguer le répéteur';
@override
String get contacts_roomPathTrace =>
'Traçage du chemin vers le serveur de la salle';
String get contacts_roomPathTrace => 'Traçage du chemin vers le room server';
@override
String get contacts_roomPing => 'Pinguer le serveur de la salle';
String get contacts_roomPing => 'Pinguer le room server';
@override
String get contacts_chatTraceRoute => 'Tracer le chemin';
@@ -3371,7 +3394,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_gpxExportRepeaters =>
'Exporter les répéteurs / serveur de salle au format GPX';
'Exporter les répéteurs / room servers au format GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
@@ -3409,7 +3432,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_gpxExportRepeatersRoom =>
'Emplacements des serveurs de répéteur et de salle';
'Emplacements des répéteurs et room servers';
@override
String get settings_gpxExportChat => 'Emplacements des compagnons';
@@ -3460,11 +3483,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Ajouter automatiquement les serveurs de salle';
'Ajouter automatiquement les room servers';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts';
'Autoriser le compagnon à ajouter automatiquement les room servers découverts';
@override
String get contactsSettings_autoAddSensorsTitle =>
@@ -3511,4 +3534,198 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?';
@override
String get chat_sendCooldown =>
'Veuillez patienter un instant avant de réessayer.';
@override
String get appSettings_jumpToOldestUnread =>
'Accéder au message le plus ancien non lu';
@override
String get 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.';
@override
String get appSettings_languageHu => 'Hongrois';
@override
String get appSettings_languageJa => 'Japonais';
@override
String get appSettings_languageKo => 'Coréen';
@override
String get radioStats_tooltip =>
'Statistiques des radios et des réseaux sans fil';
@override
String get radioStats_screenTitle => 'Statistiques de radio';
@override
String get radioStats_notConnected =>
'Connectez-vous à un appareil pour visualiser les statistiques de la radio.';
@override
String get radioStats_firmwareTooOld =>
'Les statistiques radio nécessitent un firmware compatible v8 ou une version ultérieure.';
@override
String get radioStats_waiting => 'En attente des données…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Niveau de bruit : $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Dernier RSSI : $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Dernier SNR : $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Temps d\'antenne à la télévision du Texas (total) : $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Temps d\'utilisation de l\'appareil RX (total) : $seconds s';
}
@override
String get radioStats_chartCaption =>
'Niveau de bruit (dBm) sur les échantillons récents.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Niveau de bruit : $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting =>
'Récupération des statistiques de la radio…';
@override
String get radioStats_settingsTile => 'Statistiques de radio';
@override
String get radioStats_settingsSubtitle =>
'Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d\'antenne';
@override
String get translation_title => 'Traduction';
@override
String get translation_enableTitle => 'Activer la traduction';
@override
String get translation_enableSubtitle =>
'Traduire les messages entrants et permettre la traduction avant l\'envoi.';
@override
String get translation_composerTitle => 'Traduire avant d\'envoyer';
@override
String get translation_composerSubtitle =>
'Contrôle l\'état par défaut de l\'icône de traduction du composant.';
@override
String get translation_targetLanguage => 'Langue cible';
@override
String get translation_useAppLanguage =>
'Utiliser la langue de l\'application';
@override
String get translation_downloadedModelLabel => 'Modèle téléchargé';
@override
String get translation_presetModelLabel => 'Modèle Hugging Face préconfiguré';
@override
String get translation_manualUrlLabel => 'URL du modèle manuel';
@override
String get translation_downloadModel => 'Télécharger le modèle';
@override
String get translation_downloading => 'Téléchargement...';
@override
String get translation_working => 'Au travail...';
@override
String get translation_stop => 'Arrêtez';
@override
String get translation_mergingChunks =>
'Fusion des fragments téléchargés dans le fichier final...';
@override
String get translation_downloadedModels => 'Modèles téléchargés';
@override
String get translation_deleteModel => 'Supprimer le modèle';
@override
String get translation_modelDownloaded => 'Modèle de traduction téléchargé.';
@override
String get translation_downloadStopped =>
'Le téléchargement a été interrompu.';
@override
String translation_downloadFailed(String error) {
return 'Échec du téléchargement : $error';
}
@override
String get translation_enterUrlFirst => 'Entrez d\'abord l\'URL du modèle.';
@override
String get scanner_linuxPairingShowPin => 'Afficher le code PIN';
@override
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
@override
String get scanner_linuxPairingPinTitle =>
'Code PIN pour la connexion Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Entrez le code PIN pour $deviceName (laissez vide si nécessaire).';
}
@override
String get translation_messageTranslation => 'Traduction du message';
@override
String get translation_translateBeforeSending => 'Traduire avant d\'envoyer';
@override
String get translation_composerEnabledHint =>
'Les messages seront traduits avant d\'être envoyés.';
@override
String get translation_composerDisabledHint =>
'Envoyez des messages dans la langue originale, telle que vous l\'avez tapée.';
@override
String translation_translateTo(String language) {
return 'Traduire en $language';
}
@override
String get translation_translationOptions => 'Options de traduction';
@override
String get translation_systemLanguage => 'Langue du système';
}
File diff suppressed because it is too large Load Diff
+224 -8
View File
@@ -437,9 +437,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Includi la posizione nell\'annuncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
String get settings_multiAck => 'ACK multipli';
@override
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
@@ -1239,6 +1237,14 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get chat_noMessages => 'Nessun messaggio ancora';
@override
String get chat_sendMessage => 'Invia messaggio';
@override
String chat_sendMessageTo(String contactName) {
return 'Invia un messaggio a $contactName';
}
@override
String get chat_sendMessageToStart => 'Invia un messaggio per iniziare';
@@ -1258,11 +1264,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...';
@@ -2013,9 +2014,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_guest => 'Informazioni sul ripetitore';
@override
String get room_guest => 'Informazioni sul server';
@override
String get repeater_managementTools => 'Strumenti di Gestione';
@override
String get repeater_guestTools => 'Strumenti per gli ospiti';
@override
String get repeater_status => 'Stato';
@@ -2050,6 +2060,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_settingsSubtitle =>
'Configura i parametri del ripetitore';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronizzazione dell\'orologio dopo il login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.';
@override
String get repeater_statusTitle => 'Stato del Ripetitore';
@@ -2426,6 +2444,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';
@@ -3491,4 +3515,196 @@ class AppLocalizationsIt extends AppLocalizations {
@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';
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+269 -54
View File
@@ -313,7 +313,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_nodeSettings => 'Node Instellingen';
@override
String get settings_nodeName => 'Node Naam';
String get settings_nodeName => 'Nodenaam';
@override
String get settings_nodeNameNotSet => 'Niet ingesteld';
@@ -432,9 +432,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Locatie opnemen in advertentie';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
String get settings_multiAck => 'Meerdere bevestigingen';
@override
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
@@ -452,7 +450,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_advertisementSent => 'Advertentie verzonden';
@override
String get settings_syncTime => 'Synchronisatie Tijd';
String get settings_syncTime => 'Tijd Synchroniseren';
@override
String get settings_syncTimeSubtitle =>
@@ -472,7 +470,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_rebootDevice => 'Apparaat opnieuw opstarten';
@override
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore apparaat';
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore-apparaat';
@override
String get settings_rebootDeviceConfirm =>
@@ -556,7 +554,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_codingRate => 'Codeertarief';
@override
String get settings_txPower => 'TX Vermogen (dBm)';
String get settings_txPower => 'TX-Vermogen (dBm)';
@override
String get settings_txPowerHelper => '0 - 22';
@@ -565,11 +563,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
@override
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
String get settings_clientRepeat => 'Off-Grid Herhalen';
@override
String get settings_clientRepeatSubtitle =>
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
'Laat dit apparaat de berichten van andere apparaten doorsturen.';
@override
String get settings_clientRepeatFreqWarning =>
@@ -846,19 +844,19 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_allTime => 'Altijd';
@override
String get appSettings_lastHour => 'Laat uur';
String get appSettings_lastHour => 'Afgelopen uur';
@override
String get appSettings_last6Hours => 'laatste 6 uur';
String get appSettings_last6Hours => 'Afgelopen 6 uur';
@override
String get appSettings_last24Hours => 'De laatste 24 uur';
String get appSettings_last24Hours => 'Afgelopen 24 uur';
@override
String get appSettings_lastWeek => 'Laatste week';
String get appSettings_lastWeek => 'Afgelopen week';
@override
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
String get appSettings_offlineMapCache => 'Offline Kaartcache';
@override
String get appSettings_unitsTitle => 'Eenheden';
@@ -1185,32 +1183,32 @@ class AppLocalizationsNl extends AppLocalizations {
String get channels_sortUnread => 'Ongelezen';
@override
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
String get channels_createPrivateChannel => 'PrivéKanaal Aanmaken';
@override
String get channels_createPrivateChannelDesc =>
'Beveiligd met een geheime sleutel.';
@override
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
String get channels_joinPrivateChannel => 'PrivéKanaal Toetreden';
@override
String get channels_joinPrivateChannelDesc =>
'Handmatig een geheime sleutel invoeren.';
'Voer handmatig een geheime sleutel in.';
@override
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
String get channels_joinPublicChannel => 'Publiek Kanaal Toetreden';
@override
String get channels_joinPublicChannelDesc =>
'Iedereen kan dit kanaal aanmelden.';
'Iedereen kan toetreden tot dit kanaal.';
@override
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
String get channels_joinHashtagChannel => 'Hashtag-kanaal Aanmaken';
@override
String get channels_joinHashtagChannelDesc =>
'Iedereen kan lid worden van hashtag-kanalen.';
'Iedereen kan toetreden tot hashtag-kanalen.';
@override
String get channels_scanQrCode => 'Scan een QR-code';
@@ -1227,6 +1225,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_noMessages => 'Nog geen berichten.';
@override
String get chat_sendMessage => 'Verzend bericht';
@override
String chat_sendMessageTo(String contactName) {
return 'Verstuur een bericht naar $contactName';
}
@override
String get chat_sendMessageToStart => 'Een bericht sturen om te beginnen';
@@ -1246,11 +1252,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_location => 'Locatie';
@override
String chat_sendMessageTo(String contactName) {
return 'Verstuur een bericht naar $contactName';
}
@override
String get chat_typeMessage => 'Type een bericht...';
@@ -1709,7 +1710,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_sharedPin => 'Gedeelde pin';
@override
String get map_joinRoom => 'Sluit Kamer';
String get map_joinRoom => 'Kamer Toetreden';
@override
String get map_manageRepeater => 'Beheer Repeater';
@@ -2001,7 +2002,16 @@ class AppLocalizationsNl extends AppLocalizations {
String get room_management => 'Beheer Server Kamer';
@override
String get repeater_managementTools => 'Beheerinstrumenten';
String get repeater_guest => 'Informatie over herhalingsapparatuur';
@override
String get room_guest => 'Informatie over de server';
@override
String get repeater_managementTools => 'Beheerfuncties';
@override
String get repeater_guestTools => 'Gastenfuncties';
@override
String get repeater_status => 'Status';
@@ -2027,7 +2037,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_neighbors => 'Buren';
@override
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
String get repeater_neighborsSubtitle => 'Bekijk nul-hopsburen.';
@override
String get repeater_settings => 'Instellingen';
@@ -2035,6 +2045,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Configureer repeaterparameters';
@override
String get repeater_clockSyncAfterLogin =>
'Na het inloggen, klok synchroniseren';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.';
@override
String get repeater_statusTitle => 'Status repeater';
@@ -2093,10 +2111,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_noiseFloor => 'Ruisvloer';
@override
String get repeater_txAirtime => 'TX Airtime';
String get repeater_txAirtime => 'TX-zendtijd';
@override
String get repeater_rxAirtime => 'RX Airtime';
String get repeater_rxAirtime => 'RX-zendtijd';
@override
String get repeater_packetStatistics => 'Pakketstatistieken';
@@ -2141,7 +2159,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_settingsTitle => 'Repeater Instellingen';
String get repeater_settingsTitle => 'Repeaterinstellingen';
@override
String get repeater_basicSettings => 'Basisinstellingen';
@@ -2150,19 +2168,19 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_repeaterName => 'Repeaternaam';
@override
String get repeater_repeaterNameHelper => 'Weergave naam voor deze repeater';
String get repeater_repeaterNameHelper => 'Weergavenaam voor deze repeater';
@override
String get repeater_adminPassword => 'Admin wachtwoord';
@override
String get repeater_adminPasswordHelper => 'Volledige toegangspaswoord';
String get repeater_adminPasswordHelper => 'Wachtwoord administratortoegang';
@override
String get repeater_guestPassword => 'Wachtwoord Gast';
String get repeater_guestPassword => 'Gast wachtwoord';
@override
String get repeater_guestPasswordHelper => 'Leesbeheer wachtwoord';
String get repeater_guestPasswordHelper => 'Wachtwoord gasttoegen';
@override
String get repeater_radioSettings => 'Radio Instellingen';
@@ -2189,7 +2207,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_codingRate => 'Codeertarief';
@override
String get repeater_locationSettings => 'Locatie Instellingen';
String get repeater_locationSettings => 'Locatie-instellingen';
@override
String get repeater_latitude => 'Breedtegraad';
@@ -2221,14 +2239,14 @@ class AppLocalizationsNl extends AppLocalizations {
'Toegestane leesbeheer toegang voor gasten.';
@override
String get repeater_privacyMode => 'Privacy Modus';
String get repeater_privacyMode => 'Privacymodus';
@override
String get repeater_privacyModeSubtitle =>
'Naam/locatie verbergen in advertenties';
@override
String get repeater_advertisementSettings => 'Advertentie Instellingen';
String get repeater_advertisementSettings => 'Advertentie-instellingen';
@override
String get repeater_localAdvertInterval => 'Lokale Advertentie Interval';
@@ -2333,7 +2351,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_refreshGuestAccess => 'Toegang Gast Vernieuwen';
@override
String get repeater_refreshPrivacyMode => 'Privacy Mode vernieuwen';
String get repeater_refreshPrivacyMode => 'Privacymode vernieuwen';
@override
String get repeater_refreshAdvertisementSettings =>
@@ -2359,10 +2377,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_commandHelp => 'Help';
@override
String get repeater_clearHistory => 'Verwijder Geschiedenis';
String get repeater_clearHistory => 'Geschiedenis Verwijderen';
@override
String get repeater_noCommandsSent => 'Geen commando\'s verzonden nog.';
String get repeater_noCommandsSent => 'Nog geen commando\'s verzonden.';
@override
String get repeater_typeCommandOrUseQuick =>
@@ -2389,28 +2407,34 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_cliQuickGetName => 'Haal Naam op';
String get repeater_cliQuickGetName => 'Naam opvragen';
@override
String get repeater_cliQuickGetRadio => 'Radio ontvangen';
String get repeater_cliQuickGetRadio => 'Radio-instellingen opvragen';
@override
String get repeater_cliQuickGetTx => 'Krijg TX';
String get repeater_cliQuickGetTx => 'TX opvragen';
@override
String get repeater_cliQuickNeighbors => 'Buren';
String get repeater_cliQuickNeighbors => 'Buren opvragen';
@override
String get repeater_cliQuickVersion => 'Versie';
String get repeater_cliQuickVersion => 'Versie opvragen';
@override
String get repeater_cliQuickAdvertise => 'Advertenties';
String get repeater_cliQuickAdvertise => 'Advertenties opvragen';
@override
String get repeater_cliQuickClock => 'Tijd';
String get repeater_cliQuickClock => 'Tijd opvragen';
@override
String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket';
String get repeater_cliQuickClockSync => 'Kloksynchronisatie';
@override
String get repeater_cliQuickDiscovery => 'Ontdek Buren';
@override
String get repeater_cliHelpAdvert => 'Advertentie uitzenden';
@override
String get repeater_cliHelpReboot =>
@@ -2682,7 +2706,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get telemetry_voltageLabel => 'Spanning';
@override
String get telemetry_mcuTemperatureLabel => 'MCU Temperatuur';
String get telemetry_mcuTemperatureLabel => 'MCU-temperatuur';
@override
String get telemetry_temperatureLabel => 'Temperatuur';
@@ -2723,7 +2747,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
String get neighbors_repeatersNeighbors => 'Repeatbburen';
@override
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
@@ -3022,7 +3046,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get listFilter_latestMessages => 'Recente berichten';
@override
String get listFilter_heardRecently => 'Hoor je onlangs';
String get listFilter_heardRecently => 'Recent gezien';
@override
String get listFilter_az => 'A-Z';
@@ -3268,7 +3292,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Contact uit klembord toevoegen';
@override
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
String get contacts_ShareContact => 'Contact naar Klembord kopiëren';
@override
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
@@ -3469,4 +3493,195 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?';
@override
String get chat_sendCooldown =>
'Gelieve even te wachten voordat u opnieuw verzendt.';
@override
String get appSettings_jumpToOldestUnread =>
'Ga naar het oudste ongelezen bericht';
@override
String get 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.';
@override
String get appSettings_languageHu => 'Hongaars';
@override
String get appSettings_languageJa => 'Japanisch';
@override
String get appSettings_languageKo => 'Koreaans';
@override
String get radioStats_tooltip => 'Statistieken voor radio en mesh-netwerken';
@override
String get radioStats_screenTitle => 'Statistieken over radio';
@override
String get radioStats_notConnected =>
'Verbind met een apparaat om radio-statistieken te bekijken.';
@override
String get radioStats_firmwareTooOld =>
'Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.';
@override
String get radioStats_waiting => 'Wacht op gegevens…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Ruisfrequentie: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Laatste RSSI-waarde: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Laatste SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX-tijd (totaal): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tijd besteed met RX (totaal): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ruisfrequentie (dBm) over recente metingen.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Ruisfrequentie: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Radio-statistieken ophalen…';
@override
String get radioStats_settingsTile => 'Statistieken over radio';
@override
String get radioStats_settingsSubtitle =>
'Ruimtelijke ruis, RSSI, SNR en beschikbare tijd';
@override
String get translation_title => 'Vertaling';
@override
String get translation_enableTitle => 'Activeer vertaling';
@override
String get translation_enableSubtitle =>
'Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.';
@override
String get translation_composerTitle => 'Vertaal voor verzending';
@override
String get translation_composerSubtitle =>
'Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.';
@override
String get translation_targetLanguage => 'Doeltaal';
@override
String get translation_useAppLanguage => 'Gebruik de taal van de app';
@override
String get translation_downloadedModelLabel => 'Gedownloade model';
@override
String get translation_presetModelLabel =>
'Voorgeprogrammeerd Hugging Face-model';
@override
String get translation_manualUrlLabel => 'URL van de handleiding';
@override
String get translation_downloadModel => 'Download het model';
@override
String get translation_downloading => 'Downloaden...';
@override
String get translation_working => 'Werken...';
@override
String get translation_stop => 'Stoppen';
@override
String get translation_mergingChunks =>
'Het samenvoegen van de gedownloade stukken tot één eindbestand...';
@override
String get translation_downloadedModels => 'Gedownloade modellen';
@override
String get translation_deleteModel => 'Model verwijderen';
@override
String get translation_modelDownloaded => 'Vertalingmodel gedownload.';
@override
String get translation_downloadStopped => 'Download is afgebroken.';
@override
String translation_downloadFailed(String error) {
return 'Download mislukt: $error';
}
@override
String get translation_enterUrlFirst =>
'Voer eerst een URL van een model in.';
@override
String get scanner_linuxPairingShowPin => 'Toon PIN';
@override
String get scanner_linuxPairingHidePin => 'PIN verbergen';
@override
String get scanner_linuxPairingPinTitle => 'BluetoothkoppelingsPIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Voer PIN in voor $deviceName (laat leeg als er geen is).';
}
@override
String get translation_messageTranslation => 'Berichtvertaling';
@override
String get translation_translateBeforeSending => 'Vertaal voor verzending';
@override
String get translation_composerEnabledHint =>
'De berichten worden vertaald voordat ze verzonden worden.';
@override
String get translation_composerDisabledHint =>
'Stuur berichten in de oorspronkelijke, getypte taal.';
@override
String translation_translateTo(String language) {
return 'Vertalen naar $language';
}
@override
String get translation_translationOptions => 'Opties voor vertaling';
@override
String get translation_systemLanguage => 'Taal van het systeem';
}
+242 -30
View File
@@ -439,9 +439,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Uwzględnij lokalizację w ogłoszeniu';
@override
String settings_multiAck(String value) {
return 'Wiele potwierdzeń: $value';
}
String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
@override
String get settings_telemetryModeUpdated =>
@@ -611,7 +609,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_language => 'Język';
@override
String get appSettings_languageSystem => 'Domyślny systemu';
String get appSettings_languageSystem => 'Domyślny systemowy';
@override
String get appSettings_languageEn => 'English';
@@ -744,42 +742,42 @@ class AppLocalizationsPl extends AppLocalizations {
'Automatyczne obracanie tras wyłączone';
@override
String get appSettings_maxRouteWeight => 'Maksymalna waga ścieżki';
String get appSettings_maxRouteWeight =>
'Maksymalny dopuszczalny ciężar pojazdu';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maksymalna waga, jaką ścieżka może osiągnąć dzięki udanym dostarczeniom';
'Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.';
@override
String get appSettings_initialRouteWeight => 'Początkowa waga ścieżki';
String get appSettings_initialRouteWeight => 'Początkowa waga trasy';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Waga początkowa dla nowo odkrytych ścieżek';
'Początkowa waga dla nowych, odkrytych ścieżek';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Przyrost wagi po sukcesie';
String get appSettings_routeWeightSuccessIncrement => 'Wzrost wagi sukcesu';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Waga dodawana do ścieżki po udanym dostarczeniu';
'Waga dodana do ścieżki po pomyślnym dostarczeniu';
@override
String get appSettings_routeWeightFailureDecrement =>
'Spadek wagi po niepowodzeniu';
'Zmniejszenie wagi kary';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Waga odejmowana od ścieżki po nieudanym dostarczeniu';
'Waga usunięta z trasy po nieudanej dostawie';
@override
String get appSettings_maxMessageRetries =>
'Maksymalna liczba ponowień wiadomości';
'Maksymalna liczba prób wysłania wiadomości';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Liczba prób ponowienia przed oznaczeniem wiadomości jako nieudanej';
'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej';
@override
String path_routeWeight(String weight, String max) {
@@ -1247,6 +1245,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_noMessages => 'Brak jeszcze wiadomości';
@override
String get chat_sendMessage => 'Wyślij wiadomość';
@override
String chat_sendMessageTo(String contactName) {
return 'Wyślij wiadomość do $contactName';
}
@override
String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.';
@@ -1267,11 +1273,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_location => 'Lokalizacja';
@override
String chat_sendMessageTo(String contactName) {
return 'Wyślij wiadomość do $contactName';
}
@override
String get chat_typeMessage => 'Wpisz wiadomość...';
@@ -1699,7 +1700,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_otherNodes => 'Inne węzły';
@override
String get map_showOverlaps => 'Nakładające się klucze powtarzalne';
String get map_showOverlaps => 'Nakładające się klucze przekaźników';
@override
String get map_keyPrefix => 'Prefiks klucza';
@@ -1745,7 +1746,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_runTrace => 'Uruchom ślad ścieżki';
@override
String get map_runTraceWithReturnPath => 'Wróć z powrotem tą samą ścieżką';
String get map_runTraceWithReturnPath => 'Wróć tą samą ścieżką';
@override
String get map_removeLast => 'Usuń ostatni';
@@ -2028,9 +2029,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_guest => 'Informacje dotyczące urządzenia powtarzającego';
@override
String get room_guest => 'Informacje o serwerze';
@override
String get repeater_managementTools => 'Narzędzia Zarządzania';
@override
String get repeater_guestTools => 'Narzędzia dla gości';
@override
String get repeater_status => 'Status';
@@ -2055,8 +2065,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_neighbors => 'Sąsiedzi';
@override
String get repeater_neighborsSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
String get repeater_neighborsSubtitle => 'Wyświetl sąsiadów zero-hop.';
@override
String get repeater_settings => 'Ustawienia';
@@ -2064,6 +2073,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronizacja zegara po zalogowaniu';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.';
@override
String get repeater_statusTitle => 'Status przekaźnika';
@@ -2436,6 +2453,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Godzina';
@override
String get repeater_cliQuickClockSync => 'Synchronizacja zegara';
@override
String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów';
@override
String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy';
@@ -3368,11 +3391,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportRepeaters =>
'Eksportuj przekaźniki / serwer pokojowy do GPX';
'Eksportuj przekaźniki / roomservery do GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.';
'Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.';
@override
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
@@ -3404,7 +3427,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportRepeatersRoom =>
'Lokalizacje przekaźników i serwerów pokojowych';
'Lokalizacje przekaźników i roomserverów';
@override
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
@@ -3421,7 +3444,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Eksport danych mapy GPX meshcore-open';
@override
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
String get snrIndicator_nearByRepeaters => 'Pobliskie przekaźniki';
@override
String get snrIndicator_lastSeen => 'Ostatnio widziany';
@@ -3454,11 +3477,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatycznie dodaj serwery pokojowe';
'Automatycznie dodaj roomservery';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.';
@override
String get contactsSettings_autoAddSensorsTitle =>
@@ -3503,4 +3526,193 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?';
@override
String get chat_sendCooldown =>
'Prosimy o chwilowe oczekiwanie przed ponownym wysłaniem.';
@override
String get appSettings_jumpToOldestUnread =>
'Przejdź do najstarszego nieodczytanej wiadomości';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Przy otwieraniu czatu z nieodczytanymi wiadomościami, przewijaj, aby przejść do pierwszej nieodczytanej wiadomości, zamiast do najnowszej.';
@override
String get appSettings_languageHu => 'Węgierski';
@override
String get appSettings_languageJa => 'Japoński';
@override
String get appSettings_languageKo => 'Koreański';
@override
String get radioStats_tooltip => 'Statystyki dotyczące radia i siatki';
@override
String get radioStats_screenTitle => 'Statystyki radiowe';
@override
String get radioStats_notConnected =>
'Połącz się z urządzeniem, aby wyświetlić statystyki radiowe.';
@override
String get radioStats_firmwareTooOld =>
'Statystyki radiowe wymagają towarzyszącej oprogramowania w wersji 8 lub nowszej.';
@override
String get radioStats_waiting => 'Czekam na dane…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Poziom szumów: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Ostatni poziom RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Ostatni poziom SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Czas emisji w stacji TX (całkowity): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Czas wykorzystania kanału RX (całkowity): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Poziom szumów (dBm) w ostatnich próbkach.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Poziom szumów: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Pobieranie danych dotyczących radia…';
@override
String get radioStats_settingsTile => 'Statystyki radiowe';
@override
String get radioStats_settingsSubtitle =>
'Szum tła, RSSI, SNR oraz czas dostępny';
@override
String get translation_title => 'Tłumaczenie';
@override
String get translation_enableTitle => 'Włącz tłumaczenie';
@override
String get translation_enableSubtitle =>
'Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.';
@override
String get translation_composerTitle => 'Przekład przed wysłaniem';
@override
String get translation_composerSubtitle =>
'Kontroluje domyślny stan ikony tłumaczenia w edytorze.';
@override
String get translation_targetLanguage => 'Język docelowy';
@override
String get translation_useAppLanguage => 'Użyj języka aplikacji';
@override
String get translation_downloadedModelLabel => 'Pobudowany model';
@override
String get translation_presetModelLabel => 'Wspólny model Hugging Face';
@override
String get translation_manualUrlLabel => 'Adres URL do wersji manualnej';
@override
String get translation_downloadModel => 'Pobierz model';
@override
String get translation_downloading => 'Pobieranie...';
@override
String get translation_working => 'Praca...';
@override
String get translation_stop => 'Zatrzymaj się';
@override
String get translation_mergingChunks =>
'Scalanie pobranych fragmentów w jeden plik końcowy...';
@override
String get translation_downloadedModels => 'Pobrane modele';
@override
String get translation_deleteModel => 'Usuń model';
@override
String get translation_modelDownloaded => 'Model tłumaczenia został pobrany.';
@override
String get translation_downloadStopped => 'Pobieranie zakończone.';
@override
String translation_downloadFailed(String error) {
return 'Nie udało się pobrać: $error';
}
@override
String get translation_enterUrlFirst => 'Najpierw wprowadź adres URL modelu.';
@override
String get scanner_linuxPairingShowPin => 'Pokaż PIN';
@override
String get scanner_linuxPairingHidePin => 'Ukryj PIN';
@override
String get scanner_linuxPairingPinTitle => 'Kod PIN parowania Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Wprowadź kod PIN dla $deviceName (pozostaw puste, jeśli brak).';
}
@override
String get translation_messageTranslation => 'Tłumaczenie wiadomości';
@override
String get translation_translateBeforeSending => 'Przekład przed wysłaniem';
@override
String get translation_composerEnabledHint =>
'Komunikaty zostaną przetłumaczone przed wysłaniem.';
@override
String get translation_composerDisabledHint =>
'Wysyłaj wiadomości w oryginalnym, wpisanym formacie.';
@override
String translation_translateTo(String language) {
return 'Tłumacz na $language';
}
@override
String get translation_translationOptions => 'Opcje tłumaczenia';
@override
String get translation_systemLanguage => 'Język systemu';
}
+222 -8
View File
@@ -436,9 +436,7 @@ class AppLocalizationsPt extends AppLocalizations {
'Incluir localização no anúncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
String get settings_multiAck => 'Multi-ACKs';
@override
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
@@ -1238,6 +1236,14 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get chat_noMessages => 'Ainda não existem mensagens.';
@override
String get chat_sendMessage => 'Enviar mensagem';
@override
String chat_sendMessageTo(String contactName) {
return 'Enviar uma mensagem para $contactName';
}
@override
String get chat_sendMessageToStart => 'Enviar uma mensagem para começar';
@@ -1257,11 +1263,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...';
@@ -2012,9 +2013,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_guest => 'Informações sobre repetidores';
@override
String get room_guest => 'Informações do Servidor';
@override
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@override
String get repeater_guestTools => 'Ferramentas para hóspedes';
@override
String get repeater_status => 'Status';
@@ -2047,6 +2057,14 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronização do relógio após o login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.';
@override
String get repeater_statusTitle => 'Status do Repetidor';
@@ -2423,6 +2441,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';
@@ -3484,4 +3508,194 @@ class AppLocalizationsPt extends AppLocalizations {
@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';
}
+222 -8
View File
@@ -436,9 +436,7 @@ class AppLocalizationsRu extends AppLocalizations {
'Включить местоположение в объявление';
@override
String settings_multiAck(String value) {
return 'Мульти-ACK: $value';
}
String get settings_multiAck => 'Несколько подтверждений';
@override
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
@@ -1238,6 +1236,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_noMessages => 'Сообщений пока нет';
@override
String get chat_sendMessage => 'Отправить сообщение';
@override
String chat_sendMessageTo(String contactName) {
return 'Отправить сообщение $contactName';
}
@override
String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать';
@@ -1257,11 +1263,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_location => 'Местоположение';
@override
String chat_sendMessageTo(String contactName) {
return 'Отправить сообщение $contactName';
}
@override
String get chat_typeMessage => 'Напишите сообщение...';
@@ -2016,9 +2017,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get room_management => 'Управление сервером комнат';
@override
String get repeater_guest => 'Информация о ретрансляторе';
@override
String get room_guest => 'Информация о сервере';
@override
String get repeater_managementTools => 'Инструменты управления';
@override
String get repeater_guestTools => 'Инструменты для гостей';
@override
String get repeater_status => 'Статус';
@@ -2051,6 +2061,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Настройка параметров репитера';
@override
String get repeater_clockSyncAfterLogin =>
'Синхронизация часов после входа в систему';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.';
@override
String get repeater_statusTitle => 'Статус репитера';
@@ -2427,6 +2445,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 => 'Отправляет пакет анонсирования';
@@ -3498,4 +3522,194 @@ class AppLocalizationsRu extends AppLocalizations {
@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 => 'Язык системы';
}
+222 -8
View File
@@ -430,9 +430,7 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
@override
String settings_multiAck(String value) {
return 'Viaceré ACK: $value';
}
String get settings_multiAck => 'Viaceré ACK';
@override
String get settings_telemetryModeUpdated =>
@@ -1226,6 +1224,14 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_noMessages => 'Zatiaľ žiadne správy.';
@override
String get chat_sendMessage => 'Odoslať správu';
@override
String chat_sendMessageTo(String contactName) {
return 'Pošli správu $contactName';
}
@override
String get chat_sendMessageToStart => 'Pošlite správu na začiatok';
@@ -1245,11 +1251,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...';
@@ -2001,9 +2002,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get room_management => 'Správa servera miestnosti';
@override
String get repeater_guest => 'Informácie o opakovači';
@override
String get room_guest => 'Informácie o serveri';
@override
String get repeater_managementTools => 'Nástroje na správu';
@override
String get repeater_guestTools => 'Nástroje pre hostí';
@override
String get repeater_status => 'Status';
@@ -2036,6 +2046,14 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronizácia hodiniek po prihlávení';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.';
@override
String get repeater_statusTitle => 'Status opakého zboru';
@@ -2406,6 +2424,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.';
@@ -3464,4 +3488,194 @@ class AppLocalizationsSk extends AppLocalizations {
@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';
}
+223 -8
View File
@@ -430,9 +430,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
@override
String settings_multiAck(String value) {
return 'Večkratni potrditvi: $value';
}
String get settings_multiAck => 'Več potrdil';
@override
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
@@ -1224,6 +1222,14 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get chat_noMessages => 'Še ni sporočil.';
@override
String get chat_sendMessage => 'Pošlji sporočilo';
@override
String chat_sendMessageTo(String contactName) {
return 'Pošlji sporočilo $contactName';
}
@override
String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.';
@@ -1244,11 +1250,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...';
@@ -1998,9 +1999,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_guest => 'Informacije o ponovljalniku';
@override
String get room_guest => 'Informacije o strežniku';
@override
String get repeater_managementTools => 'Upravne orodje';
@override
String get repeater_guestTools => 'Naložila za goste';
@override
String get repeater_status => 'Status';
@@ -2035,6 +2045,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_settingsSubtitle =>
'Konfigurirajte parametre ponovitelja';
@override
String get repeater_clockSyncAfterLogin => 'Sinhronizacija ure po prijavi';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.';
@override
String get repeater_statusTitle => 'Status ponovitelja';
@@ -2409,6 +2426,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';
@@ -3467,4 +3490,196 @@ class AppLocalizationsSl extends AppLocalizations {
@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';
}
+224 -8
View File
@@ -428,9 +428,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
String get settings_multiAck => 'Flera bekräftelser';
@override
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
@@ -1217,6 +1215,14 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get chat_noMessages => 'Inga meddelanden ännu';
@override
String get chat_sendMessage => 'Skicka meddelande';
@override
String chat_sendMessageTo(String contactName) {
return 'Skicka ett meddelande till $contactName';
}
@override
String get chat_sendMessageToStart =>
'Skicka ett meddelande för att komma igång';
@@ -1238,11 +1244,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...';
@@ -1987,9 +1988,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get room_management => 'Rumserverhantering';
@override
String get repeater_guest => 'Information om repetorer';
@override
String get room_guest => 'Information om servern';
@override
String get repeater_managementTools => 'Administrationsverktyg';
@override
String get repeater_guestTools => 'Gästverktyg';
@override
String get repeater_status => 'Status';
@@ -2022,6 +2032,14 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar';
@override
String get repeater_clockSyncAfterLogin =>
'Synkronisera klockan efter inloggning';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.';
@override
String get repeater_statusTitle => 'Återspelsstatus';
@@ -2394,6 +2412,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';
@@ -3444,4 +3468,196 @@ class AppLocalizationsSv extends AppLocalizations {
@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';
}
+223 -8
View File
@@ -432,9 +432,7 @@ class AppLocalizationsUk extends AppLocalizations {
'Включити місце розташування в оголошення';
@override
String settings_multiAck(String value) {
return 'Багатократне підтвердження: $value';
}
String get settings_multiAck => 'Багато підтверджень';
@override
String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
@@ -1230,6 +1228,14 @@ class AppLocalizationsUk 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 => 'Надішліть повідомлення, щоб почати';
@@ -1250,11 +1256,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get chat_location => 'Розташування';
@override
String chat_sendMessageTo(String contactName) {
return 'Надіслати повідомлення $contactName';
}
@override
String get chat_typeMessage => 'Введіть повідомлення...';
@@ -2011,9 +2012,18 @@ class AppLocalizationsUk 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 => 'Статус';
@@ -2047,6 +2057,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора';
@override
String get repeater_clockSyncAfterLogin => 'Синхронізація годин після входу';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.';
@override
String get repeater_statusTitle => 'Статус ретранслятора';
@@ -2427,6 +2444,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Годинник';
@override
String get repeater_cliQuickClockSync => 'Синхронізація годинника';
@override
String get repeater_cliQuickDiscovery => 'Відкрити сусідів';
@override
String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення';
@@ -3501,4 +3524,196 @@ class AppLocalizationsUk extends AppLocalizations {
@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 дБм';
}
@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 => 'Мова системи';
}
+208 -8
View File
@@ -408,9 +408,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get settings_advertLocationSubtitle => '在广告中包含位置';
@override
String settings_multiAck(String value) {
return '多重ACK$value';
}
String get settings_multiAck => '多重ACK';
@override
String get settings_telemetryModeUpdated => '遥测模式已更新';
@@ -1161,6 +1159,14 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get chat_noMessages => '暂无消息';
@override
String get chat_sendMessage => '发送消息';
@override
String chat_sendMessageTo(String contactName) {
return '发送消息给 $contactName';
}
@override
String get chat_sendMessageToStart => '发送消息开始对话';
@@ -1180,11 +1186,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get chat_location => '位置';
@override
String chat_sendMessageTo(String contactName) {
return '发送消息给 $contactName';
}
@override
String get chat_typeMessage => '输入消息...';
@@ -1887,9 +1888,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get room_management => '房间服务器管理';
@override
String get repeater_guest => '重复器信息';
@override
String get room_guest => '服务器信息';
@override
String get repeater_managementTools => '管理工具';
@override
String get repeater_guestTools => '访客工具';
@override
String get repeater_status => '状态';
@@ -1920,6 +1930,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_settingsSubtitle => '配置转发节点参数';
@override
String get repeater_clockSyncAfterLogin => '登录后,自动同步时钟';
@override
String get repeater_clockSyncAfterLoginSubtitle => '在成功登录后,自动发送“时钟同步”指令。';
@override
String get repeater_statusTitle => '转发节点状态';
@@ -2277,6 +2293,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 => '发送广播包';
@@ -3222,4 +3244,182 @@ class AppLocalizationsZh extends AppLocalizations {
@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 => '系统语言';
}
+179 -56
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",
@@ -107,13 +107,13 @@
"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 +145,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 +232,11 @@
"appSettings_mapTimeFilter": "Filter tijd op kaart",
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
"appSettings_allTime": "Altijd",
"appSettings_lastHour": "Laat uur",
"appSettings_last6Hours": "laatste 6 uur",
"appSettings_last24Hours": "De laatste 24 uur",
"appSettings_lastWeek": "Laatste week",
"appSettings_offlineMapCache": "Offline Kaarten Cache",
"appSettings_lastHour": "Afgelopen uur",
"appSettings_last6Hours": "Afgelopen 6 uur",
"appSettings_last24Hours": "Afgelopen 24 uur",
"appSettings_lastWeek": "Afgelopen week",
"appSettings_offlineMapCache": "Offline Kaartcache",
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
"@appSettings_areaSelectedZoom": {
@@ -682,7 +682,7 @@
"map_showSharedMarkers": "Toon gedeelde markeringen",
"map_lastSeenTime": "Laatste Bekeken Tijd",
"map_sharedPin": "Gedeelde pin",
"map_joinRoom": "Sluit Kamer",
"map_joinRoom": "Kamer Toetreden",
"map_manageRepeater": "Beheer Repeater",
"mapCache_title": "Offline Kaarten Cache",
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
@@ -878,7 +878,7 @@
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
"path_setPath": "Stel Pad in",
"repeater_management": "Beheer Repeaters",
"repeater_managementTools": "Beheerinstrumenten",
"repeater_managementTools": "Beheerfuncties",
"repeater_status": "Status",
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
"repeater_telemetry": "Telemetry",
@@ -912,8 +912,8 @@
"repeater_lastRssi": "Laatste RSSI",
"repeater_lastSnr": "Laatste SNR",
"repeater_noiseFloor": "Ruisvloer",
"repeater_txAirtime": "TX Airtime",
"repeater_rxAirtime": "RX Airtime",
"repeater_txAirtime": "TX-zendtijd",
"repeater_rxAirtime": "RX-zendtijd",
"repeater_packetStatistics": "Pakketstatistieken",
"repeater_sent": "Verzonden",
"repeater_received": "Ontvangen",
@@ -982,14 +982,14 @@
}
}
},
"repeater_settingsTitle": "Repeater Instellingen",
"repeater_settingsTitle": "Repeaterinstellingen",
"repeater_basicSettings": "Basisinstellingen",
"repeater_repeaterName": "Repeaternaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
"repeater_repeaterNameHelper": "Weergavenaam voor deze repeater",
"repeater_adminPassword": "Admin wachtwoord",
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
"repeater_guestPassword": "Wachtwoord Gast",
"repeater_guestPasswordHelper": "Leesbeheer wachtwoord",
"repeater_adminPasswordHelper": "Wachtwoord administratortoegang",
"repeater_guestPassword": "Gast wachtwoord",
"repeater_guestPasswordHelper": "Wachtwoord gasttoegen",
"repeater_radioSettings": "Radio Instellingen",
"repeater_frequencyMhz": "Frequentie (MHz)",
"repeater_frequencyHelper": "300-2500 MHz",
@@ -998,7 +998,7 @@
"repeater_bandwidth": "Bandbreedte",
"repeater_spreadingFactor": "Spreidingsfactor",
"repeater_codingRate": "Codeertarief",
"repeater_locationSettings": "Locatie Instellingen",
"repeater_locationSettings": "Locatie-instellingen",
"repeater_latitude": "Breedtegraad",
"repeater_latitudeHelper": "Graadseconden (bijv. 37.7749)",
"repeater_longitude": "Lengtegraad",
@@ -1008,9 +1008,9 @@
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
"repeater_guestAccess": "Toegang voor Gasten",
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
"repeater_privacyMode": "Privacy Modus",
"repeater_privacyMode": "Privacymodus",
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"repeater_advertisementSettings": "Advertentie Instellingen",
"repeater_advertisementSettings": "Advertentie-instellingen",
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
"repeater_localAdvertIntervalMinutes": "{minutes} minuten",
"@repeater_localAdvertIntervalMinutes": {
@@ -1073,7 +1073,7 @@
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
"repeater_refreshGuestAccess": "Toegang Gast Vernieuwen",
"repeater_refreshPrivacyMode": "Privacy Mode vernieuwen",
"repeater_refreshPrivacyMode": "Privacymode vernieuwen",
"repeater_refreshAdvertisementSettings": "Instellingen Advertentie Bijwerken",
"repeater_refreshed": "{label} is vernieuwd",
"@repeater_refreshed": {
@@ -1094,8 +1094,8 @@
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Debug Volgende Commando",
"repeater_commandHelp": "Help",
"repeater_clearHistory": "Verwijder Geschiedenis",
"repeater_noCommandsSent": "Geen commando's verzonden nog.",
"repeater_clearHistory": "Geschiedenis Verwijderen",
"repeater_noCommandsSent": "Nog geen commando's verzonden.",
"repeater_typeCommandOrUseQuick": "Typ een opdracht hieronder of gebruik snelle commando's",
"repeater_enterCommandHint": "Voer bevel in...",
"repeater_previousCommand": "Vorige opdracht",
@@ -1110,14 +1110,14 @@
}
}
},
"repeater_cliQuickGetName": "Haal Naam op",
"repeater_cliQuickGetRadio": "Radio ontvangen",
"repeater_cliQuickGetTx": "Krijg TX",
"repeater_cliQuickNeighbors": "Buren",
"repeater_cliQuickVersion": "Versie",
"repeater_cliQuickAdvertise": "Advertenties",
"repeater_cliQuickClock": "Tijd",
"repeater_cliHelpAdvert": "Verstuurt een advertentiepakket",
"repeater_cliQuickGetName": "Naam opvragen",
"repeater_cliQuickGetRadio": "Radio-instellingen opvragen",
"repeater_cliQuickGetTx": "TX opvragen",
"repeater_cliQuickNeighbors": "Buren opvragen",
"repeater_cliQuickVersion": "Versie opvragen",
"repeater_cliQuickAdvertise": "Advertenties opvragen",
"repeater_cliQuickClock": "Tijd opvragen",
"repeater_cliHelpAdvert": "Advertentie uitzenden",
"repeater_cliHelpReboot": "Herstart het apparaat. (let op, je krijgt mogelijk een 'Timeout', wat normaal is)",
"repeater_cliHelpClock": "Toont de huidige tijd per apparaat's klok.",
"repeater_cliHelpPassword": "Stelt een nieuw beheerderswachtwoord in voor het apparaat.",
@@ -1203,7 +1203,7 @@
},
"telemetry_batteryLabel": "Batterij",
"telemetry_voltageLabel": "Spanning",
"telemetry_mcuTemperatureLabel": "MCU Temperatuur",
"telemetry_mcuTemperatureLabel": "MCU-temperatuur",
"telemetry_temperatureLabel": "Temperatuur",
"telemetry_currentLabel": "Huidig",
"telemetry_batteryValue": "{percent}% / {volts}V",
@@ -1346,7 +1346,7 @@
"listFilter_tooltip": "Filteren en sorteren",
"listFilter_sortBy": "Sorteren door",
"listFilter_latestMessages": "Recente berichten",
"listFilter_heardRecently": "Hoor je onlangs",
"listFilter_heardRecently": "Recent gezien",
"listFilter_az": "A-Z",
"listFilter_filters": "Filters",
"listFilter_all": "Alles",
@@ -1363,20 +1363,20 @@
}
},
"repeater_neighbors": "Buren",
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
"repeater_neighborsSubtitle": "Bekijk nul-hopsburen.",
"neighbors_receivedData": "Ontvangen Buurdata",
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
"neighbors_repeatersNeighbors": "Herhalingen Buren",
"neighbors_repeatersNeighbors": "Repeatbburen",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
"channels_createPrivateChannel": "PrivéKanaal Aanmaken",
"channels_joinPrivateChannel": "PrivéKanaal Toetreden",
"channels_joinPrivateChannelDesc": "Voer handmatig een geheime sleutel in.",
"channels_joinPublicChannel": "Publiek Kanaal Toetreden",
"channels_joinPublicChannelDesc": "Iedereen kan toetreden tot dit kanaal.",
"channels_joinHashtagChannel": "Hashtag-kanaal Aanmaken",
"channels_joinHashtagChannelDesc": "Iedereen kan toetreden tot hashtag-kanalen.",
"channels_scanQrCode": "Scan een QR-code",
"channels_scanQrCodeComingSoon": "Komt later",
"channels_enterHashtag": "Voer hashtag in",
@@ -1574,7 +1574,7 @@
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
"contacts_ShareContact": "Contact naar Klembord kopiëren",
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
"notification_activityTitle": "MeshCore Activiteit",
@@ -1612,8 +1612,8 @@
"snrIndicator_lastSeen": "Laatst gezien",
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
"chat_ShowAllPaths": "Toon alle paden",
"settings_clientRepeat": "Herhalen: Afgekoppeld",
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
"settings_clientRepeat": "Off-Grid Herhalen",
"settings_clientRepeatSubtitle": "Laat dit apparaat de berichten van andere apparaten doorsturen.",
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Eenheden",
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Laatst gezien",
"contact_clearChat": "Chat leegmaken",
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.",
"appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route",
"appSettings_maxRouteWeight": "Maximale gewicht voor de route",
@@ -1941,7 +1934,137 @@
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Herhalingssleutel overlapt",
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad."
}
"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",
"settings_multiAck": "Meerdere bevestigingen"
}
+141 -18
View File
@@ -163,7 +163,7 @@
"appSettings_themeLight": "Jasne",
"appSettings_themeDark": "Ciemny",
"appSettings_language": "Język",
"appSettings_languageSystem": "Domyślny systemu",
"appSettings_languageSystem": "Domyślny systemowy",
"appSettings_languageEn": "English",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
@@ -1373,7 +1373,7 @@
}
},
"repeater_neighbors": "Sąsiedzi",
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"repeater_neighborsSubtitle": "Wyświetl sąsiadów zero-hop.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
@@ -1622,12 +1622,12 @@
},
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / serwer pokojowy do GPX",
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.",
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / roomservery do GPX",
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.",
"settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.",
"settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym",
"settings_gpxExportError": "Wystąpił błąd podczas eksportowania.",
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i serwerów pokojowych",
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i roomserverów",
"settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.",
"settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX",
"settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.",
@@ -1648,7 +1648,7 @@
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
"scanner_enableBluetooth": "Włącz Bluetooth",
"snrIndicator_lastSeen": "Ostatnio widziany",
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
"snrIndicator_nearByRepeaters": "Pobliskie przekaźniki",
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
@@ -1846,12 +1846,12 @@
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie przekaźników",
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie odkrytych przekaźników.",
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj roomservery",
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
"settings_contactSettings": "Ustawienia kontaktów",
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.",
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
"discoveredContacts_contactAdded": "Kontakt dodany",
@@ -1960,13 +1960,6 @@
"contact_settings": "Ustawienia kontaktowe",
"contact_lastSeen": "Ostatnio widziany",
"contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Początkowa waga trasy",
"appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu",
"appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek",
@@ -1979,7 +1972,137 @@
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
"settings_multiAck": "Wiele potwierdzeń: {value}",
"map_showOverlaps": "Nakładające się klucze powtarzalne",
"map_runTraceWithReturnPath": "Wróć z powrotem tą samą ścieżką"
"map_showOverlaps": "Nakładające się klucze przekaźników",
"map_runTraceWithReturnPath": "Wróć tą samą ścieżką",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"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": "Węgierski",
"appSettings_jumpToOldestUnreadSubtitle": "Przy otwieraniu czatu z nieodczytanymi wiadomościami, przewijaj, aby przejść do pierwszej nieodczytanej wiadomości, zamiast do najnowszej.",
"appSettings_jumpToOldestUnread": "Przejdź do najstarszego nieodczytanej wiadomości",
"chat_sendCooldown": "Prosimy o chwilowe oczekiwanie przed ponownym wysłaniem.",
"appSettings_languageJa": "Japoński",
"appSettings_languageKo": "Koreański",
"radioStats_tooltip": "Statystyki dotyczące radia i siatki",
"radioStats_screenTitle": "Statystyki radiowe",
"radioStats_notConnected": "Połącz się z urządzeniem, aby wyświetlić statystyki radiowe.",
"radioStats_firmwareTooOld": "Statystyki radiowe wymagają towarzyszącej oprogramowania w wersji 8 lub nowszej.",
"radioStats_waiting": "Czekam na dane…",
"radioStats_noiseFloor": "Poziom szumów: {noiseDbm} dBm",
"radioStats_lastRssi": "Ostatni poziom RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Ostatni poziom SNR: {snr} dB",
"radioStats_txAir": "Czas emisji w stacji TX (całkowity): {seconds} s",
"radioStats_rxAir": "Czas wykorzystania kanału RX (całkowity): {seconds} s",
"radioStats_chartCaption": "Poziom szumów (dBm) w ostatnich próbkach.",
"radioStats_stripNoise": "Poziom szumów: {noiseDbm} dBm",
"radioStats_stripWaiting": "Pobieranie danych dotyczących radia…",
"radioStats_settingsTile": "Statystyki radiowe",
"radioStats_settingsSubtitle": "Szum tła, RSSI, SNR oraz czas dostępny",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Przekład przed wysłaniem",
"translation_title": "Tłumaczenie",
"translation_enableTitle": "Włącz tłumaczenie",
"translation_enableSubtitle": "Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.",
"translation_composerSubtitle": "Kontroluje domyślny stan ikony tłumaczenia w edytorze.",
"translation_targetLanguage": "Język docelowy",
"translation_useAppLanguage": "Użyj języka aplikacji",
"translation_downloadedModelLabel": "Pobudowany model",
"translation_presetModelLabel": "Wspólny model Hugging Face",
"translation_manualUrlLabel": "Adres URL do wersji manualnej",
"translation_downloadModel": "Pobierz model",
"translation_downloading": "Pobieranie...",
"translation_working": "Praca...",
"translation_stop": "Zatrzymaj się",
"translation_mergingChunks": "Scalanie pobranych fragmentów w jeden plik końcowy...",
"translation_downloadedModels": "Pobrane modele",
"translation_deleteModel": "Usuń model",
"translation_modelDownloaded": "Model tłumaczenia został pobrany.",
"translation_downloadStopped": "Pobieranie zakończone.",
"translation_downloadFailed": "Nie udało się pobrać: {error}",
"translation_enterUrlFirst": "Najpierw wprowadź adres URL modelu.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Komunikaty zostaną przetłumaczone przed wysłaniem.",
"translation_translateBeforeSending": "Przekład przed wysłaniem",
"translation_composerDisabledHint": "Wysyłaj wiadomości w oryginalnym, wpisanym formacie.",
"translation_messageTranslation": "Tłumaczenie wiadomości",
"translation_translationOptions": "Opcje tłumaczenia",
"translation_systemLanguage": "Język systemu",
"translation_translateTo": "Tłumacz na {language}",
"scanner_linuxPairingShowPin": "Pokaż PIN",
"scanner_linuxPairingHidePin": "Ukryj PIN",
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).",
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth",
"repeater_cliQuickClockSync": "Synchronizacja zegara",
"repeater_cliQuickDiscovery": "Odkryj Sąsiadów",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Synchronizacja zegara po zalogowaniu",
"repeater_clockSyncAfterLoginSubtitle": "Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.",
"chat_sendMessage": "Wyślij wiadomość",
"repeater_guestTools": "Narzędzia dla gości",
"repeater_guest": "Informacje dotyczące urządzenia powtarzającego",
"room_guest": "Informacje o serwerze",
"settings_multiAck": "Wielokrotne potwierdzenia odbioru"
}
+133 -10
View File
@@ -1922,13 +1922,6 @@
"contact_telemetry": "Telemetria",
"contact_settings": "Configurações de Contato",
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Peso Inicial da Rota",
"appSettings_maxRouteWeight": "Peso Máximo da Rota",
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
@@ -1941,7 +1934,137 @@
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
"settings_multiAck": "Multi-ACKs: {value}",
"settings_multiAck": "Multi-ACKs",
"map_showOverlaps": "Sobreposições da Chave Repeater",
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho."
}
"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"
}
+133 -10
View File
@@ -1162,13 +1162,6 @@
"contact_clearChat": "Очистить чат",
"contact_lastSeen": "Последний раз видели",
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
@@ -1181,7 +1174,137 @@
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"settings_multiAck": "Мульти-ACK: {value}",
"map_showOverlaps": "Перекрытия ключа повтора",
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути"
}
"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": "Инструменты для гостей",
"settings_multiAck": "Несколько подтверждений"
}
+133 -10
View File
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Naposledy videný",
"contact_teleBase": "Báza telemetrie",
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
"appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
"appSettings_initialRouteWeight": "Počiatočná váha trasy",
@@ -1941,7 +1934,137 @@
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
"settings_multiAck": "Viaceré ACK: {value}",
"settings_multiAck": "Viaceré ACK",
"map_showOverlaps": "Prekrývanie opakovača kľúča",
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste."
}
"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í"
}
+133 -10
View File
@@ -1922,13 +1922,6 @@
"contact_teleEnv": "Okolje telemetrije",
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
"appSettings_initialRouteWeight": "Izvirna teža poti",
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
@@ -1940,8 +1933,138 @@
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Večkratni potrditvi: {value}",
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti."
}
"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",
"settings_multiAck": "Več potrdil"
}
+133 -10
View File
@@ -1922,13 +1922,6 @@
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
"contact_teleLoc": "Telemetridata plats",
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
"appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
@@ -1941,7 +1934,137 @@
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Repeater-nyckelöverlappningar",
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg"
}
"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",
"settings_multiAck": "Flera bekräftelser"
}
+133 -10
View File
@@ -1922,13 +1922,6 @@
"contact_lastSeen": "Останній раз бачили",
"contact_teleEnv": "Середовище телеметрії",
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Початкова вартість маршруту",
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
"appSettings_maxRouteWeight": "Максимальна вага маршруту",
@@ -1941,7 +1934,137 @@
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"settings_multiAck": "Багатократне підтвердження: {value}",
"map_showOverlaps": "Перекриття ключа повторювача",
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом"
}
"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_jumpToOldestUnreadSubtitle": "При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.",
"appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення",
"appSettings_languageJa": "Японська",
"appSettings_languageKo": "Кореєська",
"radioStats_tooltip": "Статистика радіо та мережі",
"radioStats_screenTitle": "Дані про радіостанції",
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.",
"radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.",
"radioStats_waiting": "Очікую на отримання даних…",
"radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм",
"radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм",
"radioStats_lastSnr": "Останній показник SNR: {snr} дБ",
"radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд",
"radioStats_rxAir": "Загальний час використання RX: {seconds} секунд",
"radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.",
"radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм",
"radioStats_stripWaiting": "Отримано статистику радіо…",
"radioStats_settingsTile": "Дані про радіостанції",
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Перекладіть перед відправкою",
"translation_title": "Переклад",
"translation_enableTitle": "Увімкнути переклад",
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
"translation_targetLanguage": "Цільова мова",
"translation_useAppLanguage": "Використовуйте мову додатку",
"translation_downloadedModelLabel": "Завантажений шаблон",
"translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face",
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
"translation_downloadModel": "Завантажити модель",
"translation_downloading": "Завантаження...",
"translation_working": "Працюю...",
"translation_stop": "Припинити",
"translation_mergingChunks": "Об'єднання завантажених фрагментів у кінцевий файл...",
"translation_downloadedModels": "Завантажені моделі",
"translation_deleteModel": "Видалити модель",
"translation_modelDownloaded": "Модель перекладу завантажена.",
"translation_downloadStopped": "Завантаження призупинено.",
"translation_downloadFailed": "Не вдалося завантажити: {error}",
"translation_enterUrlFirst": "Спочатку введіть URL моделі.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Повідомлення будуть перекладені перед відправленням.",
"translation_messageTranslation": "Переклад повідомлення",
"translation_composerDisabledHint": "Надсилайте повідомлення, використовуючи оригінальний текстовий формат.",
"translation_translateBeforeSending": "Перекладіть перед відправкою",
"translation_translateTo": "Перекласти на {language}",
"translation_translationOptions": "Варіанти перекладу",
"translation_systemLanguage": "Мова системи",
"scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
"scanner_linuxPairingShowPin": "Показати PIN",
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN",
"repeater_cliQuickClockSync": "Синхронізація годинника",
"repeater_cliQuickDiscovery": "Відкрити сусідів",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
"repeater_guestTools": "Інструменти для гостей",
"repeater_guest": "Інформація про ретранслятор",
"room_guest": "Інформація про сервер кімнати",
"chat_sendMessage": "Надіслати повідомлення",
"settings_multiAck": "Багато підтверджень"
}
+133 -10
View File
@@ -1927,13 +1927,6 @@
"contact_settings": "联系人设置",
"contact_teleLocSubtitle": "允许共享位置数据",
"contact_telemetry": "遥测数据",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeight": "最大路径重量",
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
"appSettings_initialRouteWeight": "初始路线权重",
@@ -1945,8 +1938,138 @@
"appSettings_maxMessageRetries": "最大消息重试次数",
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "多重ACK{value}",
"settings_multiAck": "多重ACK",
"settings_telemetryModeUpdated": "遥测模式已更新",
"map_showOverlaps": "重复键重叠",
"map_runTraceWithReturnPath": "沿着相同的路径返回"
}
"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": "服务器信息"
}
+8
View File
@@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart';
import 'services/background_service.dart';
import 'services/map_tile_cache_service.dart';
import 'services/chat_text_scale_service.dart';
import 'services/translation_service.dart';
import 'services/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart';
@@ -41,6 +42,7 @@ void main() async {
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
final translationService = TranslationService(appSettingsService);
final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage);
@@ -60,6 +62,7 @@ void main() async {
_registerThirdPartyLicenses();
await chatTextScaleService.initialize();
await translationService.refreshDownloadedModels();
await uiViewStateService.initialize();
await timeoutPredictionService.initialize();
@@ -68,6 +71,7 @@ void main() async {
retryService: retryService,
pathHistoryService: pathHistoryService,
appSettingsService: appSettingsService,
translationService: translationService,
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
backgroundService: backgroundService,
@@ -93,6 +97,7 @@ void main() async {
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
translationService: translationService,
uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService,
),
@@ -130,6 +135,7 @@ class MeshCoreApp extends StatelessWidget {
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
final TranslationService translationService;
final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService;
@@ -144,6 +150,7 @@ class MeshCoreApp extends StatelessWidget {
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
required this.translationService,
required this.uiViewStateService,
required this.timeoutPredictionService,
});
@@ -159,6 +166,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
ChangeNotifierProvider.value(value: translationService),
ChangeNotifierProvider.value(value: uiViewStateService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),
+68 -1
View File
@@ -1,3 +1,5 @@
import 'translation_support.dart';
enum UnitSystem { metric, imperial }
extension UnitSystemValue on UnitSystem {
@@ -48,6 +50,13 @@ class AppSettings {
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,
@@ -84,9 +93,17 @@ class AppSettings {
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 {
@@ -124,6 +141,15 @@ class AppSettings {
'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(),
};
}
@@ -192,6 +218,25 @@ class AppSettings {
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 [],
);
}
@@ -230,6 +275,13 @@ class AppSettings {
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,
@@ -278,6 +330,21 @@ class AppSettings {
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,
);
}
}
+39 -2
View File
@@ -2,6 +2,7 @@ import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart';
import 'translation_support.dart';
import '../utils/app_logger.dart';
enum ChannelMessageStatus { pending, sent, failed }
@@ -24,9 +25,16 @@ class Repeat {
}
class ChannelMessage {
static const Object _unset = Object();
final Uint8List? senderKey;
final String senderName;
final String text;
final String? originalText;
final String? translatedText;
final String? translatedLanguageCode;
final MessageTranslationStatus translationStatus;
final String? translationModelId;
final DateTime timestamp;
final bool isOutgoing;
final ChannelMessageStatus status;
@@ -47,6 +55,11 @@ class ChannelMessage {
this.senderKey,
required this.senderName,
required this.text,
this.originalText,
this.translatedText,
this.translatedLanguageCode,
this.translationStatus = MessageTranslationStatus.none,
this.translationModelId,
required this.timestamp,
required this.isOutgoing,
this.status = ChannelMessageStatus.pending,
@@ -86,12 +99,30 @@ class ChannelMessage {
String? replyToMessageId,
String? replyToSenderName,
String? replyToText,
Object? originalText = _unset,
Object? translatedText = _unset,
Object? translatedLanguageCode = _unset,
MessageTranslationStatus? translationStatus,
Object? translationModelId = _unset,
Map<String, int>? reactions,
}) {
return ChannelMessage(
senderKey: senderKey,
senderName: senderName,
text: text,
originalText: originalText == _unset
? this.originalText
: originalText as String?,
translatedText: translatedText == _unset
? this.translatedText
: translatedText as String?,
translatedLanguageCode: translatedLanguageCode == _unset
? this.translatedLanguageCode
: translatedLanguageCode as String?,
translationStatus: translationStatus ?? this.translationStatus,
translationModelId: translationModelId == _unset
? this.translationModelId
: translationModelId as String?,
timestamp: timestamp,
isOutgoing: isOutgoing,
status: status ?? this.status,
@@ -191,12 +222,18 @@ class ChannelMessage {
static ChannelMessage outgoing(
String text,
String senderName,
int channelIndex,
) {
int channelIndex, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
}) {
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
status: ChannelMessageStatus.pending,
+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;
}
}
}
+8 -6
View File
@@ -119,15 +119,14 @@ class Contact {
);
}
String get pathIdList {
/// 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
@@ -138,6 +137,9 @@ 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)}>";
}
+43 -3
View File
@@ -1,19 +1,27 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import 'translation_support.dart';
enum MessageStatus { pending, sent, delivered, failed }
class Message {
static const Object _unset = Object();
final Uint8List senderKey;
final String text;
final DateTime timestamp;
final bool isOutgoing;
final bool isCli;
final MessageStatus status;
final String? originalText;
final String? translatedText;
final String? translatedLanguageCode;
final MessageTranslationStatus translationStatus;
final String? translationModelId;
// NEW: Retry logic fields
final String? messageId;
final String messageId;
final int retryCount;
final int? estimatedTimeoutMs;
final int? expectedAckHash;
@@ -33,7 +41,12 @@ class Message {
required this.isOutgoing,
this.isCli = false,
this.status = MessageStatus.pending,
this.messageId,
String? messageId,
this.originalText,
this.translatedText,
this.translatedLanguageCode,
this.translationStatus = MessageTranslationStatus.none,
this.translationModelId,
this.retryCount = 0,
this.estimatedTimeoutMs,
this.expectedAckHash,
@@ -45,7 +58,10 @@ class Message {
Uint8List? fourByteRoomContactKey,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
}) : pathBytes = pathBytes ?? Uint8List(0),
}) : messageId =
messageId ??
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
reactions = reactions ?? {},
reactionStatuses = reactionStatuses ?? {};
@@ -63,6 +79,11 @@ class Message {
int? pathLength,
Uint8List? pathBytes,
bool? isCli,
Object? originalText = _unset,
Object? translatedText = _unset,
Object? translatedLanguageCode = _unset,
MessageTranslationStatus? translationStatus,
Object? translationModelId = _unset,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
Uint8List? fourByteRoomContactKey,
@@ -75,6 +96,19 @@ class Message {
isCli: isCli ?? this.isCli,
status: status ?? this.status,
messageId: messageId,
originalText: originalText == _unset
? this.originalText
: originalText as String?,
translatedText: translatedText == _unset
? this.translatedText
: translatedText as String?,
translatedLanguageCode: translatedLanguageCode == _unset
? this.translatedLanguageCode
: translatedLanguageCode as String?,
translationStatus: translationStatus ?? this.translationStatus,
translationModelId: translationModelId == _unset
? this.translationModelId
: translationModelId as String?,
retryCount: retryCount ?? this.retryCount,
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
@@ -124,12 +158,18 @@ class Message {
static Message outgoing(
Uint8List recipientKey,
String text, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
int? pathLength,
Uint8List? pathBytes,
}) {
return Message(
senderKey: recipientKey,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
isCli: false,
+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,
+618 -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,15 +327,14 @@ class AppSettingsScreen extends StatelessWidget {
value: settingsService.settings.autoRouteRotationEnabled,
onChanged: (value) {
settingsService.setAutoRouteRotationEnabled(value);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
value
? context.l10n.appSettings_autoRouteRotationEnabled
: context.l10n.appSettings_autoRouteRotationDisabled,
),
duration: const Duration(seconds: 2),
showDismissibleSnackBar(
context,
content: Text(
value
? context.l10n.appSettings_autoRouteRotationEnabled
: context.l10n.appSettings_autoRouteRotationDisabled,
),
duration: const Duration(seconds: 2),
);
},
),
@@ -522,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,
@@ -689,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;
}
@@ -776,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',
),
],
),
),
@@ -811,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,
),
],
),
@@ -863,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,
),
],
),
@@ -884,6 +1135,126 @@ 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,
// TODO: l10n
content: Text('Deleted ${translationModelFriendlyName(model)}.'),
);
} catch (error) {
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text('Delete failed: $error'),
); // TODO: l10n
}
}
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,
@@ -907,15 +1278,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),
);
},
),
@@ -924,3 +1294,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),
),
],
);
}
}
+4 -4
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,
+174 -47
View File
@@ -11,21 +11,27 @@ 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 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -47,6 +53,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
bool _isLoadingOlder = false;
MeshCoreConnector? _connector;
DateTime? _lastChannelSendAt;
bool _channelSkipNextBottomSnap = false;
@override
void initState() {
@@ -55,11 +63,45 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_scrollController.onScrollNearTop = _loadOlderMessages;
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 = connector.getUnreadCountForChannelIndex(idx);
ChannelMessage? anchor;
if (settings.jumpToOldestUnread && unread > 0) {
anchor = _findOldestUnreadChannelAnchor(
connector.getChannelMessages(widget.channel),
unread,
);
}
connector.setActiveChannel(idx);
_connector = connector;
if (anchor != null) {
_channelSkipNextBottomSnap = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
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 _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
@@ -103,11 +145,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;
}
@@ -167,6 +208,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
centerTitle: false,
actions: [
const RadioStatsIconButton(),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
@@ -243,6 +285,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();
});
@@ -310,8 +356,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing;
final gifId = _parseGifId(message.text);
final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(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
@@ -462,16 +516,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: LinkHandler.buildLinkifyText(
context: context,
text: message.text,
child: TranslatedMessageContent(
displayText: translatedDisplayText,
originalText: originalDisplayText,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
originalStyle: TextStyle(
fontSize: bodyFontSize * textScale,
color: Colors.green,
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.72),
),
),
),
@@ -645,7 +700,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final colorScheme = Theme.of(context).colorScheme;
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
final gifId = _parseGifId(replyText);
final gifId = GifHelper.parseGif(replyText);
final poi = _parsePoiMessage(replyText);
Widget contentPreview;
@@ -757,12 +812,6 @@ 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(
@@ -843,7 +892,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
isScrollControlled: true,
builder: (context) => GifPicker(
onGifSelected: (gifId) {
_textController.text = 'g:$gifId';
_textController.text = GifHelper.encodeGif(gifId);
},
),
);
@@ -957,6 +1006,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: [
@@ -988,11 +1038,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,
@@ -1037,27 +1093,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(),
);
},
),
@@ -1065,6 +1127,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,
),
@@ -1075,29 +1138,91 @@ 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) {
@@ -1204,23 +1329,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) {
+18 -3
View File
@@ -64,6 +64,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
flipPathAround: true,
reversePathAround:
!(!channelMessage && !message.isOutgoing),
pathHashByteWidth: context
.read<MeshCoreConnector>()
.pathHashByteWidth,
),
),
),
@@ -819,7 +822,8 @@ List<_PathHop> _buildPathHops(
) {
if (pathBytes.isEmpty) return const [];
final candidatesByPrefix = <int, List<Contact>>{};
for (final contact in connector.allContacts) {
final allContacts = connector.allContacts;
for (final contact in allContacts) {
if (contact.publicKey.isEmpty) continue;
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
continue;
@@ -836,7 +840,8 @@ List<_PathHop> _buildPathHops(
: null;
var previousPosition = startPoint;
final distance = Distance();
var lastDistance = 0.0;
var bestDistance = 0.0;
final hops = <_PathHop>[];
for (var i = 0; i < pathBytes.length; i++) {
final searchPoint = i == 0 ? startPoint : previousPosition;
@@ -845,7 +850,7 @@ List<_PathHop> _buildPathHops(
if (candidates != null && candidates.isNotEmpty) {
var bestIndex = 0;
if (searchPoint != null) {
var bestDistance = double.infinity;
bestDistance = double.infinity;
for (var j = 0; j < candidates.length; j++) {
final candidate = candidates[j];
if (!candidate.hasLocation ||
@@ -873,6 +878,16 @@ List<_PathHop> _buildPathHops(
if (resolvedPosition != null) {
previousPosition = resolvedPosition;
}
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
if (lastDistance + bestDistance > 50000 &&
candidates != null &&
candidates.isNotEmpty) {
i--;
lastDistance = bestDistance;
continue;
}
lastDistance = bestDistance;
hops.add(
_PathHop(
index: i + 1,
+78 -109
View File
@@ -24,6 +24,7 @@ import '../widgets/empty_state.dart';
import '../widgets/qr_code_display.dart';
import '../widgets/quick_switch_bar.dart';
import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart';
import 'contacts_screen.dart';
@@ -809,15 +810,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(
dialogContext,
).showSnackBar(
SnackBar(
content: Text(
dialogContext
.l10n
.channels_enterChannelName,
),
showDismissibleSnackBar(
context,
content: Text(
dialogContext
.l10n
.channels_enterChannelName,
),
);
return;
@@ -837,13 +835,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
nextIndex,
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelAdded(
name,
),
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_channelAdded(name),
),
);
}
@@ -897,15 +892,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final name = nameController.text.trim();
final pskHex = pskController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(
dialogContext,
).showSnackBar(
SnackBar(
content: Text(
dialogContext
.l10n
.channels_enterChannelName,
),
showDismissibleSnackBar(
context,
content: Text(
dialogContext
.l10n
.channels_enterChannelName,
),
);
return;
@@ -914,15 +906,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try {
psk = Channel.parsePskHex(pskHex);
} on FormatException {
ScaffoldMessenger.of(
dialogContext,
).showSnackBar(
SnackBar(
content: Text(
dialogContext
.l10n
.channels_pskMustBe32Hex,
),
showDismissibleSnackBar(
context,
content: Text(
dialogContext
.l10n
.channels_pskMustBe32Hex,
),
);
return;
@@ -930,13 +919,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelAdded(
name,
),
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_channelAdded(name),
),
);
}
@@ -967,11 +953,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, 'Public', psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_publicChannelAdded,
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_publicChannelAdded,
),
);
}
@@ -1097,15 +1082,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async {
var hashtag = hashtagController.text.trim();
if (hashtag.isEmpty) {
ScaffoldMessenger.of(
dialogContext,
).showSnackBar(
SnackBar(
content: Text(
dialogContext
.l10n
.channels_enterChannelName,
),
showDismissibleSnackBar(
context,
content: Text(
dialogContext
.l10n
.channels_enterChannelName,
),
);
return;
@@ -1125,15 +1107,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
} else {
// Community hashtag - HMAC derivation from community secret
if (selectedCommunity == null) {
ScaffoldMessenger.of(
showDismissibleSnackBar(
dialogContext,
).showSnackBar(
SnackBar(
content: Text(
dialogContext
.l10n
.community_selectCommunity,
),
content: Text(
dialogContext
.l10n
.community_selectCommunity,
),
);
return;
@@ -1159,12 +1138,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
psk,
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelAdded(
channelName,
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_channelAdded(
channelName,
),
),
);
@@ -1259,13 +1237,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(
dialogContext,
).showSnackBar(
SnackBar(
content: Text(
dialogContext.l10n.community_enterName,
),
showDismissibleSnackBar(
context,
content: Text(
dialogContext.l10n.community_enterName,
),
);
return;
@@ -1301,11 +1276,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
_loadCommunities();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.community_created(name),
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.community_created(name),
),
);
@@ -1494,10 +1468,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try {
psk = Channel.parsePskHex(pskHex);
} on FormatException {
ScaffoldMessenger.of(dialogContext).showSnackBar(
SnackBar(
content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
),
showDismissibleSnackBar(
dialogContext,
content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
);
return;
}
@@ -1510,16 +1483,16 @@ 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('Failed to update channel: $e'),
);
}
},
@@ -1559,21 +1532,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleted(channel.name),
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_channelDeleted(channel.name),
),
);
} catch (e, st) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleteFailed(channel.name),
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.channels_channelDeleteFailed(channel.name),
),
);
@@ -1594,8 +1565,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
final psk = Channel.parsePskHex(Channel.publicChannelPsk);
connector.setChannel(0, 'Public', psk);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.channels_publicChannelAdded)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.channels_publicChannelAdded),
);
}
@@ -1810,12 +1782,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)),
);
}
},
+214 -79
View File
@@ -16,18 +16,20 @@ import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart';
import '../helpers/link_handler.dart';
import '../helpers/gif_helper.dart';
import '../helpers/path_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_history.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/path_history_service.dart';
import '../services/translation_service.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/elements_ui.dart';
import '../widgets/byte_count_input.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
import '../utils/emoji_utils.dart';
@@ -35,9 +37,13 @@ import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_translation_button.dart';
import '../widgets/path_selection_dialog.dart';
import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart';
import '../utils/app_logger.dart';
import '../l10n/l10n.dart';
import '../helpers/snack_bar_builder.dart';
import 'telemetry_screen.dart';
class ChatScreen extends StatefulWidget {
@@ -53,8 +59,11 @@ class _ChatScreenState extends State<ChatScreen> {
final _textController = TextEditingController();
final _scrollController = ChatScrollController();
final _textFieldFocusNode = FocusNode();
final GlobalKey _unreadScrollKey = GlobalKey();
bool _isLoadingOlder = false;
MeshCoreConnector? _connector;
Message? _pendingUnreadScrollTarget;
DateTime? _lastTextSendAt;
@override
void initState() {
@@ -63,11 +72,50 @@ class _ChatScreenState extends State<ChatScreen> {
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_connector = context.read<MeshCoreConnector>();
_connector?.setActiveContact(widget.contact.publicKeyHex);
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final keyHex = widget.contact.publicKeyHex;
final unread = connector.getUnreadCountForContactKey(keyHex);
Message? anchor;
if (settings.jumpToOldestUnread && unread > 0) {
anchor = _findOldestUnreadAnchor(
connector.getMessages(widget.contact),
unread,
);
}
connector.setActiveContact(keyHex);
_connector = connector;
if (anchor != null) {
setState(() => _pendingUnreadScrollTarget = anchor);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final ctx = _unreadScrollKey.currentContext;
if (ctx != null) {
Scrollable.ensureVisible(
ctx,
duration: const Duration(milliseconds: 350),
alignment: 0.15,
);
}
setState(() => _pendingUnreadScrollTarget = null);
});
}
});
}
Message? _findOldestUnreadAnchor(List<Message> messages, int unreadCount) {
if (unreadCount <= 0 || messages.isEmpty) return null;
var n = 0;
Message? oldest;
for (final m in messages.reversed) {
if (m.isOutgoing || m.isCli) continue;
n++;
oldest = m;
if (n >= unreadCount) break;
}
return oldest;
}
void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
@@ -247,6 +295,7 @@ class _ChatScreenState extends State<ChatScreen> {
tooltip: context.l10n.chat_pathManagement,
onPressed: () => _showPathHistory(context),
),
const RadioStatsIconButton(),
Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
return PopupMenuButton<String>(
@@ -378,6 +427,7 @@ class _ChatScreenState extends State<ChatScreen> {
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (_pendingUnreadScrollTarget != null) return;
_scrollController.scrollToBottomIfAtBottom();
});
@@ -424,7 +474,7 @@ class _ChatScreenState extends State<ChatScreen> {
(service) => service.scale,
);
final resolvedContact = _resolveContact(connector);
return _MessageBubble(
final bubble = _MessageBubble(
message: message,
senderName: resolvedContact.type == advTypeRoom
? "${contact.name} [$fourByteHex]"
@@ -436,6 +486,10 @@ class _ChatScreenState extends State<ChatScreen> {
onRetryReaction: (msg, emoji) =>
_sendReaction(msg, contact, emoji),
);
if (identical(message, _pendingUnreadScrollTarget)) {
return KeyedSubtree(key: _unreadScrollKey, child: bubble);
}
return bubble;
},
);
},
@@ -446,6 +500,7 @@ class _ChatScreenState extends State<ChatScreen> {
Widget _buildInputBar(MeshCoreConnector connector) {
final maxBytes = maxContactMessageBytes();
final colorScheme = Theme.of(context).colorScheme;
final settings = context.watch<AppSettingsService>().settings;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
@@ -460,11 +515,17 @@ class _ChatScreenState extends State<ChatScreen> {
onPressed: () => _showGifPicker(context),
tooltip: context.l10n.chat_sendGif,
),
if (settings.translationEnabled)
MessageTranslationButton(
enabled: settings.composerTranslationEnabled,
languageCode: settings.translationTargetLanguageCode,
onPressed: _showTranslationOptions,
),
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
builder: (context, value, child) {
final gifId = _parseGifId(value.text);
final gifId = GifHelper.parseGif(value.text);
if (gifId != null) {
return Focus(
autofocus: true,
@@ -506,24 +567,35 @@ class _ChatScreenState extends State<ChatScreen> {
),
);
}
return TextField(
return ByteCountedTextField(
maxBytes: maxBytes,
controller: _textController,
focusNode: _textFieldFocusNode,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(maxBytes),
],
textCapitalization: TextCapitalization.sentences,
hintText: context.l10n.chat_typeMessage,
onSubmitted: (_) => _sendMessage(connector),
encoder:
connector.isContactSmazEnabled(
widget.contact.publicKeyHex,
)
? (text) => connector.prepareContactOutboundText(
widget.contact,
text,
)
: null,
decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage,
border: const OutlineInputBorder(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
filled: true,
fillColor: Theme.of(
context,
).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
horizontal: 20,
vertical: 14,
),
),
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(connector),
);
},
),
@@ -531,6 +603,9 @@ class _ChatScreenState extends State<ChatScreen> {
const SizedBox(width: 8),
IconButton.filled(
icon: const Icon(Icons.send),
tooltip: context.l10n.chat_sendMessageTo(
_resolveContact(connector).name,
),
onPressed: () => _sendMessage(connector),
),
],
@@ -539,39 +614,96 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
void _showGifPicker(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => GifPicker(
onGifSelected: (gifId) {
_textController.text = 'g:$gifId';
_textController.text = GifHelper.encodeGif(gifId);
},
),
);
}
void _sendMessage(MeshCoreConnector connector) {
Future<void> _showTranslationOptions() async {
final settingsService = context.read<AppSettingsService>();
final settings = settingsService.settings;
await showMessageTranslationSheet(
context: context,
enabled: settings.composerTranslationEnabled,
selectedLanguageCode: settings.translationTargetLanguageCode,
onEnabledChanged: settingsService.setComposerTranslationEnabled,
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
);
}
Future<void> _sendMessage(MeshCoreConnector connector) async {
final text = _textController.text.trim();
if (text.isEmpty) return;
final now = DateTime.now();
if (_lastTextSendAt != null &&
now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_sendCooldown),
);
return;
}
_lastTextSendAt = now;
final settings = context.read<AppSettingsService>().settings;
final translationService = context.read<TranslationService>();
var outgoingText = text;
String? originalText;
String? translatedLanguageCode;
String? translationModelId;
if (settings.translationEnabled) {
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
Localizations.localeOf(context).languageCode,
);
if (translationService.shouldTranslateOutgoing(
text: text,
targetLanguageCode: targetLanguageCode,
)) {
final result = await translationService.translateOutgoingText(
text: text,
targetLanguageCode: targetLanguageCode,
);
if (!mounted) return;
if (result != null &&
result.status == MessageTranslationStatus.completed &&
result.translatedText.isNotEmpty) {
outgoingText = result.translatedText;
originalText = text;
translatedLanguageCode = result.targetLanguageCode;
translationModelId = result.modelId;
}
}
}
final maxBytes = maxContactMessageBytes();
if (utf8.encode(text).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
final outboundText = connector.prepareContactOutboundText(
_resolveContact(connector),
outgoingText,
);
if (utf8.encode(outboundText).length > maxBytes) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
);
return;
}
connector.sendMessage(_resolveContact(connector), text);
_textController.clear();
_textFieldFocusNode.requestFocus();
connector.sendMessage(
_resolveContact(connector),
outgoingText,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
}
void _showPathHistory(BuildContext context) {
@@ -746,15 +878,12 @@ class _ChatScreenState extends State<ChatScreen> {
_showFullPathDialog(context, path.pathBytes),
onTap: () async {
if (path.pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context
.l10n
.chat_pathDetailsNotAvailable,
),
duration: const Duration(seconds: 2),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.chat_pathDetailsNotAvailable,
),
duration: const Duration(seconds: 2),
);
return;
}
@@ -838,11 +967,10 @@ class _ChatScreenState extends State<ChatScreen> {
_resolveContact(connector),
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_pathCleared),
duration: const Duration(seconds: 2),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_pathCleared),
duration: const Duration(seconds: 2),
);
Navigator.pop(context);
},
@@ -868,11 +996,10 @@ class _ChatScreenState extends State<ChatScreen> {
pathLen: -1,
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_floodModeEnabled),
duration: const Duration(seconds: 2),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_floodModeEnabled),
duration: const Duration(seconds: 2),
);
Navigator.pop(context);
},
@@ -906,11 +1033,10 @@ class _ChatScreenState extends State<ChatScreen> {
void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
if (pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2),
);
return;
}
@@ -950,6 +1076,7 @@ class _ChatScreenState extends State<ChatScreen> {
path: Uint8List.fromList(pathBytes),
flipPathAround: true,
targetContact: widget.contact,
pathHashByteWidth: connector.pathHashByteWidth,
),
),
),
@@ -1022,11 +1149,10 @@ class _ChatScreenState extends State<ChatScreen> {
: (verified
? context.l10n.chat_pathDeviceConfirmed
: context.l10n.chat_pathDeviceNotConfirmed);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_pathSetHops(hopCount, status)),
duration: const Duration(seconds: 3),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_pathSetHops(hopCount, status)),
duration: const Duration(seconds: 3),
);
}
@@ -1212,7 +1338,9 @@ class _ChatScreenState extends State<ChatScreen> {
connector.getContacts();
}
final pathForInput = currentContact.pathIdList;
final pathForInput = currentContact.pathFormattedIdList(
connector.pathHashByteWidth,
);
final currentPathLabel = _currentPathLabel(currentContact);
// Filter out the current contact from available contacts
@@ -1373,26 +1501,29 @@ class _ChatScreenState extends State<ChatScreen> {
void _copyMessageText(String text) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied)));
content: Text(context.l10n.chat_messageCopied),
);
}
Future<void> _deleteMessage(Message message) async {
await context.read<MeshCoreConnector>().deleteMessage(message);
if (!mounted) return;
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted)));
content: Text(context.l10n.chat_messageDeleted),
);
}
void _retryMessage(Message message) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Retry using the contact's current path override setting
connector.sendMessage(_resolveContact(connector), message.text);
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
content: Text(context.l10n.chat_retryingMessage),
);
}
void _showEmojiPicker(Message message, Contact senderContact) {
@@ -1424,7 +1555,7 @@ class _ChatScreenState extends State<ChatScreen> {
senderName,
message.text,
);
final reactionText = 'r:$hash:$emojiIndex';
final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
connector.sendMessage(_resolveContact(connector), reactionText);
}
}
@@ -1454,7 +1585,7 @@ class _MessageBubble extends StatelessWidget {
final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing;
final colorScheme = Theme.of(context).colorScheme;
final gifId = _parseGifId(message.text);
final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(message.text);
final isFailed = message.status == MessageStatus.failed;
final bubbleColor = isFailed
@@ -1471,6 +1602,14 @@ class _MessageBubble extends StatelessWidget {
if (isRoomServer && !isOutgoing) {
messageText = message.text.substring(4.clamp(0, message.text.length));
}
final translatedDisplayText =
message.translatedText != null &&
message.translatedText!.trim().isNotEmpty
? message.translatedText!.trim()
: messageText;
final originalDisplayText = isOutgoing
? message.originalText
: (translatedDisplayText != messageText ? messageText : null);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
@@ -1600,16 +1739,15 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: LinkHandler.buildLinkifyText(
context: context,
text: messageText,
child: TranslatedMessageContent(
displayText: translatedDisplayText,
originalText: originalDisplayText,
style: TextStyle(
color: textColor,
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
color: Colors.green,
decoration: TextDecoration.underline,
originalStyle: TextStyle(
color: textColor.withValues(alpha: 0.78),
fontSize: bodyFontSize * textScale,
),
),
@@ -1640,7 +1778,10 @@ class _MessageBubble extends StatelessWidget {
child: Text(
context.l10n.chat_retryCount(
message.retryCount,
4,
context
.read<AppSettingsService>()
.settings
.maxMessageRetries,
),
style: TextStyle(
fontSize: 10,
@@ -1718,12 +1859,6 @@ class _MessageBubble extends StatelessWidget {
);
}
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
_PoiInfo? _parsePoiMessage(String text) {
final trimmed = text.trim();
final match = RegExp(
+14 -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.
///
@@ -76,11 +77,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red,
);
}
} finally {
@@ -93,12 +93,11 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
}
void _showInvalidQrError(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
);
}
@@ -229,11 +228,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green,
);
// Return to previous screen
@@ -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;
}
}
+82 -73
View File
@@ -27,6 +27,7 @@ import '../widgets/quick_switch_bar.dart';
import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
import 'discovery_screen.dart';
@@ -150,9 +151,10 @@ class _ContactsScreenState extends State<ContactsScreen>
}
void _showGroupsUnavailableMessage(BuildContext context) {
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.common_loading)));
content: Text(context.l10n.common_loading),
);
}
void _setupFrameListener() {
@@ -169,10 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
// Validate packet has expected minimum size (98+ bytes per protocol)
if (advertPacket.length < 98) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_invalidAdvertFormat),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_invalidAdvertFormat),
);
}
_pendingOperations.remove(ContactOperationType.export);
@@ -187,24 +188,23 @@ class _ContactsScreenState extends State<ContactsScreen>
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImported)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactImported),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactAdvertCopied),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactAdvertCopied),
);
}
@@ -216,25 +216,22 @@ class _ContactsScreenState extends State<ContactsScreen>
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactImportFailed),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactImportFailed),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
);
}
@@ -271,8 +268,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final clipboardData = await Clipboard.getData('text/plain');
if (clipboardData == null || clipboardData.text == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_clipboardEmpty),
);
}
return;
@@ -280,8 +278,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final text = clipboardData.text!.trim();
if (!text.startsWith('meshcore://')) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_invalidAdvertFormat),
);
}
return;
@@ -294,8 +293,9 @@ class _ContactsScreenState extends State<ContactsScreen>
connector.importContact(importContactFrame);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_invalidAdvertFormat),
);
}
}
@@ -330,10 +330,9 @@ class _ContactsScreenState extends State<ContactsScreen>
),
onTap: () => {
connector.sendSelfAdvert(flood: false),
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.settings_advertisementSent),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
),
},
),
@@ -347,10 +346,9 @@ class _ContactsScreenState extends State<ContactsScreen>
),
onTap: () => {
connector.sendSelfAdvert(flood: true),
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.settings_advertisementSent),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_advertisementSent),
),
},
),
@@ -394,7 +392,7 @@ class _ContactsScreenState extends State<ContactsScreen>
children: [
const Icon(Icons.person_add_rounded),
const SizedBox(width: 8),
Text("Discovered Contacts"),
Text(context.l10n.discoveredContacts_Title),
],
),
onTap: () => Navigator.push(
@@ -963,13 +961,16 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context,
builder: (context) => RepeaterLoginDialog(
repeater: repeater,
onLogin: (password) {
onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
isAdmin: isAdmin,
),
),
);
},
@@ -986,14 +987,18 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
onLogin: (password, isAdmin) {
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password)
? RepeaterHubScreen(
repeater: room,
password: password,
isAdmin: isAdmin,
)
: ChatScreen(contact: room),
),
);
@@ -1146,19 +1151,17 @@ class _ContactsScreenState extends State<ContactsScreen>
onPressed: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_groupNameRequired),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_groupNameRequired),
);
return;
}
if (name.toLowerCase() ==
contactsAllGroupsValue.toLowerCase()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_groupNameReserved),
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_groupNameReserved),
);
return;
}
@@ -1167,11 +1170,10 @@ class _ContactsScreenState extends State<ContactsScreen>
return g.name.toLowerCase() == name.toLowerCase();
});
if (exists) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.contacts_groupAlreadyExists(name),
),
showDismissibleSnackBar(
context,
content: Text(
context.l10n.contacts_groupAlreadyExists(name),
),
);
return;
@@ -1240,20 +1242,19 @@ class _ContactsScreenState extends State<ContactsScreen>
if (isRepeater) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathBytesForDisplay.isNotEmpty
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
title: Text(context.l10n.contacts_ping),
onTap: () {
final hw = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: contact.pathBytesForDisplay.isNotEmpty
? context.l10n.contacts_repeaterPathTrace
: context.l10n.contacts_repeaterPing,
path: contact.pathBytesForDisplay,
flipPathAround: true,
title: context.l10n.contacts_repeaterPing,
path: Uint8List.fromList([contact.publicKey.first]),
targetContact: contact,
pathHashByteWidth: hw,
),
),
);
@@ -1270,10 +1271,11 @@ class _ContactsScreenState extends State<ContactsScreen>
] else if (isRoom) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathLength > 0
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
title: Text(context.l10n.contacts_pathTrace),
onTap: () {
final hw = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
@@ -1281,9 +1283,12 @@ class _ContactsScreenState extends State<ContactsScreen>
title: contact.pathBytesForDisplay.isNotEmpty
? context.l10n.contacts_roomPathTrace
: context.l10n.contacts_roomPing,
path: contact.pathBytesForDisplay,
path: contact.pathBytesForDisplay.isNotEmpty
? contact.pathBytesForDisplay
: Uint8List.fromList([contact.publicKey.first]),
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
targetContact: contact,
pathHashByteWidth: hw,
),
),
);
@@ -1318,6 +1323,9 @@ class _ContactsScreenState extends State<ContactsScreen>
leading: const Icon(Icons.radar, color: Colors.green),
title: Text(context.l10n.contacts_chatTraceRoute),
onTap: () {
final hw = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
@@ -1328,6 +1336,7 @@ class _ContactsScreenState extends State<ContactsScreen>
path: contact.pathBytesForDisplay,
flipPathAround: true,
targetContact: contact,
pathHashByteWidth: hw,
),
),
);
-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);
}
}
+61 -7
View File
@@ -12,6 +12,7 @@ import '../utils/contact_search.dart';
import '../utils/platform_info.dart';
import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart';
import '../helpers/snack_bar_builder.dart';
enum DiscoverySortOption { lastSeen, name, type }
@@ -38,6 +39,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
super.dispose();
}
DateTime _resolveLastSeen(Contact contact) {
if (contact.type != advTypeChat) return contact.lastSeen;
return contact.lastMessageAt.isAfter(contact.lastSeen)
? contact.lastMessageAt
: contact.lastSeen;
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
@@ -108,11 +116,56 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
_formatLastSeen(context, contact.lastSeen),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
// Clamp text scaling in trailing section to prevent overflow while
// maintaining accessibility. Primary content (title/subtitle) scales normally.
trailing: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(
MediaQuery.textScalerOf(
context,
).scale(1.0).clamp(1.0, 1.3),
),
),
child: SizedBox(
width: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
_formatLastSeen(
context,
_resolveLastSeen(contact),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (contact.hasLocation)
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[400],
),
if (contact.rawPacket != null)
const SizedBox(width: 2),
if (contact.rawPacket != null)
Icon(
Icons.cell_tower,
size: 14,
color: Colors.grey[400],
),
],
),
],
),
),
),
onTap: () {
@@ -182,8 +235,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
final hexString = pubKeyToHex(contact.rawPacket!);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.contacts_contactAdvertCopied),
);
break;
case 'delete_contact':
+11 -9
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
class MapCacheScreen extends StatefulWidget {
const MapCacheScreen({super.key});
@@ -112,15 +113,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
Future<void> _startDownload() async {
final bounds = _selectedBounds;
if (bounds == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.mapCache_selectAreaFirst)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.mapCache_selectAreaFirst),
);
return;
}
if (_estimatedTiles == 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.mapCache_noTilesToDownload)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.mapCache_noTilesToDownload),
);
return;
}
@@ -182,9 +185,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
result.failed,
)
: context.l10n.mapCache_cachedTiles(result.downloaded);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
showDismissibleSnackBar(context, content: Text(message));
}
Future<void> _clearCache() async {
@@ -210,8 +211,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
final cacheService = context.read<MapTileCacheService>();
await cacheService.clearCache();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.mapCache_offlineCacheCleared)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.mapCache_offlineCacheCleared),
);
}
+70 -31
View File
@@ -29,6 +29,7 @@ import 'chat_screen.dart';
import 'contacts_screen.dart';
import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import '../helpers/snack_bar_builder.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
import 'line_of_sight_map_screen.dart';
@@ -64,6 +65,7 @@ class _MapScreenState extends State<MapScreen> {
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
final List<int> _pathTrace = [];
final List<Contact> _pathTraceContacts = [];
final List<LatLng> _points = [];
final List<Polyline> _polylines = [];
bool _legendExpanded = false;
@@ -488,7 +490,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
),
if (!_isBuildingPathTrace)
if (!settings.mapShowOverlaps)
..._buildGuessedMarker(
guessedLocations,
showLabels: _showNodeLabels,
@@ -788,17 +790,26 @@ class _MapScreenState extends State<MapScreen> {
final markers = <Marker>[];
for (final guess in guessed) {
if (guess.contact.type == advTypeChat && _isBuildingPathTrace) {
continue;
}
final color = _getNodeColor(guess.contact.type);
final marker = Marker(
point: guess.position,
width: 35,
height: 35,
child: GestureDetector(
onTap: () => _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
onLongPress: () => _isBuildingPathTrace
? _showNodeInfo(context, guess.contact)
: null,
onTap: () => _isBuildingPathTrace
? _addToPath(context, guess.contact, position: guess.position)
: _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
@@ -870,23 +881,29 @@ class _MapScreenState extends State<MapScreen> {
addContact = true;
}
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
if (contact.type == advTypeChat && _isBuildingPathTrace) {
addContact = false;
}
if (settings.mapShowOverlaps) {
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
addContact = false;
}
}
if (addContact) {
filtered.add(contact);
}
@@ -1350,13 +1367,16 @@ class _MapScreenState extends State<MapScreen> {
context: context,
builder: (context) => RepeaterLoginDialog(
repeater: repeater,
onLogin: (password) {
onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
isAdmin: isAdmin,
),
),
);
},
@@ -1369,7 +1389,8 @@ class _MapScreenState extends State<MapScreen> {
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
// onLogin(password, isAdmin) isAdmin not used for room caht screen
onLogin: (password, _) {
// Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
@@ -1643,7 +1664,10 @@ class _MapScreenState extends State<MapScreen> {
);
await connector.refreshDeviceInfo();
if (!mounted) return;
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
showDismissibleSnackBar(
messenger.context,
content: Text(successMsg),
);
},
),
ListTile(
@@ -1665,8 +1689,9 @@ class _MapScreenState extends State<MapScreen> {
required String flags,
}) async {
if (!connector.isConnected) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.map_connectToShareMarkers)),
showDismissibleSnackBar(
context,
content: Text(context.l10n.map_connectToShareMarkers),
);
return;
}
@@ -2121,12 +2146,18 @@ class _MapScreenState extends State<MapScreen> {
}
}
void _addToPath(BuildContext context, Contact contact) {
void _addToPath(BuildContext context, Contact contact, {LatLng? position}) {
setState(() {
_pathTrace.add(
contact.publicKey[0],
); // Add first 16 bytes of public key to path trace
_points.add(LatLng(contact.latitude!, contact.longitude!));
_pathTraceContacts.add(
contact.copyWith(
latitude: position?.latitude ?? contact.latitude,
longitude: position?.longitude ?? contact.longitude,
),
); // Add contact to path trace contacts
_points.add(position ?? LatLng(contact.latitude!, contact.longitude!));
});
}
@@ -2134,6 +2165,7 @@ class _MapScreenState extends State<MapScreen> {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_pathTraceContacts.clear();
_points.clear();
_polylines.clear();
_points.add(position);
@@ -2142,6 +2174,7 @@ class _MapScreenState extends State<MapScreen> {
void _removePath() {
setState(() {
_pathTraceContacts.removeLast();
_pathTrace.removeLast(); // Remove last node from path trace
_points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines
@@ -2191,12 +2224,17 @@ class _MapScreenState extends State<MapScreen> {
if (_pathTrace.isNotEmpty)
IconButton(
onPressed: () {
final hashW = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
pathHashByteWidth: hashW,
pathContacts: _pathTraceContacts,
),
),
);
@@ -2242,8 +2280,9 @@ class _MapScreenState extends State<MapScreen> {
_points.clear();
_polylines.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
showDismissibleSnackBar(
context,
content: Text(l10n.map_pathTraceCancelled),
);
},
tooltip: l10n.common_cancel,
+14 -16
View File
@@ -11,6 +11,7 @@ import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart';
import '../helpers/snack_bar_builder.dart';
class NeighborsScreen extends StatefulWidget {
final Contact repeater;
@@ -142,7 +143,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final contacts = connector.allContacts;
final contacts = connector.allContactsUnfiltered;
try {
final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
@@ -163,11 +164,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_neighborCount = neighborCount;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
);
_statusTimeout?.cancel();
if (!mounted) return;
@@ -224,11 +224,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red,
);
_recordStatusResult(false);
});
@@ -239,11 +238,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red,
);
}
}
+48 -11
View File
@@ -55,6 +55,8 @@ class PathTraceMapScreen extends StatefulWidget {
final bool flipPathAround;
final bool reversePathAround;
final Contact? targetContact;
final int pathHashByteWidth;
final List<Contact>? pathContacts;
const PathTraceMapScreen({
super.key,
@@ -64,6 +66,8 @@ class PathTraceMapScreen extends StatefulWidget {
this.flipPathAround = false,
this.reversePathAround = false,
this.targetContact,
this.pathHashByteWidth = pathHashSize,
this.pathContacts,
});
@override
@@ -72,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget {
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
static const double _labelZoomThreshold = 8.5;
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
@@ -119,8 +125,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Uint8List traceBytes;
if (pathBytes.isEmpty) {
final pk = widget.targetContact?.publicKey;
final n = widget.pathHashByteWidth.clamp(1, pubKeySize);
if (pk != null && pk.length >= n) {
return Uint8List.fromList(pk.sublist(0, n));
}
traceBytes = Uint8List(1);
traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0;
traceBytes[0] = pk?[0] ?? 0;
return traceBytes;
}
@@ -259,17 +270,43 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toList();
Map<int, Contact> pathContacts = {};
final contacts = connector.allContacts;
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
for (var repeaterData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([repeaterData]),
)) {
pathContacts[repeaterData] = repeater;
Contact lastContact = Contact(
path: Uint8List(0),
pathLength: 0,
publicKey: connector.selfPublicKey ?? Uint8List(0),
name: context.l10n.pathTrace_you,
type: advTypeChat,
latitude: connector.selfLatitude,
longitude: connector.selfLongitude,
lastSeen: DateTime.now(),
);
if (widget.pathContacts != null) {
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
} else {
final contacts = connector.allContactsUnfiltered;
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
if (lastContact.latitude != null &&
lastContact.longitude != null &&
repeater.hasLocation &&
lastContact.hasLocation &&
Distance().distance(
LatLng(lastContact.latitude!, lastContact.longitude!),
LatLng(repeater.latitude!, repeater.longitude!),
) >
_maxRepeaterMatchDistanceMeters) {
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
}
}
});
for (var repeaterData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([repeaterData]),
)) {
pathContacts[repeaterData] = repeater;
lastContact = repeater;
}
}
});
}
// For hops with no GPS contact, infer position from other contacts
// with known GPS that share the same last-hop byte.
+11 -3
View File
@@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart';
import '../widgets/debug_frame_viewer.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterCliScreen extends StatefulWidget {
final Contact repeater;
@@ -35,13 +36,15 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
// Common commands for quick access
late final List<Map<String, String>> _quickCommands = [
{'labelKey': 'advertise', 'command': 'advert'},
{'labelKey': 'getName', 'command': 'get name'},
{'labelKey': 'getRadio', 'command': 'get radio'},
{'labelKey': 'getTx', 'command': 'get tx'},
{'labelKey': 'discovery', 'command': 'discover.neighbors'},
{'labelKey': 'neighbors', 'command': 'neighbors'},
{'labelKey': 'version', 'command': 'ver'},
{'labelKey': 'advertise', 'command': 'advert'},
{'labelKey': 'clock', 'command': 'clock'},
{'labelKey': 'clock sync', 'command': 'clock sync'},
];
@override
@@ -334,8 +337,9 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
if (_commandController.text.trim().isNotEmpty) {
_sendCommand(showDebug: true);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.repeater_enterCommandFirst)),
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_enterCommandFirst),
);
}
},
@@ -407,6 +411,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
return l10n.repeater_cliQuickAdvertise;
case 'clock':
return l10n.repeater_cliQuickClock;
case 'clock sync':
return l10n.repeater_cliQuickClockSync;
case 'discovery':
return l10n.repeater_cliQuickDiscovery;
default:
return key;
}
+105 -91
View File
@@ -13,11 +13,13 @@ import 'neighbors_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
final String password;
final bool isAdmin;
const RepeaterHubScreen({
super.key,
required this.repeater,
required this.password,
required this.isAdmin,
});
@override
@@ -33,11 +35,18 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
if (isAdmin)
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
if (!isAdmin)
Text(
repeater.type == advTypeRepeater
? l10n.repeater_guest
: l10n.room_guest,
),
Text(
repeater.name,
style: const TextStyle(
@@ -113,64 +122,67 @@ class RepeaterHubScreen extends StatelessWidget {
),
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.battery_full),
const SizedBox(width: 10),
Expanded(
child: Text(
l10n.appSettings_batteryChemistry,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
if (isAdmin)
Card(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.battery_full),
const SizedBox(width: 10),
Expanded(
child: Text(
l10n.appSettings_batteryChemistry,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: chemistry,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
],
),
onChanged: (value) {
if (value == null) return;
settingsService.setBatteryChemistryForRepeater(
repeater.publicKeyHex,
value,
);
},
items: [
DropdownMenuItem(
value: 'nmc',
child: Text(l10n.appSettings_batteryNmc),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: chemistry,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
),
DropdownMenuItem(
value: 'lifepo4',
child: Text(l10n.appSettings_batteryLifepo4),
),
DropdownMenuItem(
value: 'lipo',
child: Text(l10n.appSettings_batteryLipo),
),
],
),
],
onChanged: (value) {
if (value == null) return;
settingsService.setBatteryChemistryForRepeater(
repeater.publicKeyHex,
value,
);
},
items: [
DropdownMenuItem(
value: 'nmc',
child: Text(l10n.appSettings_batteryNmc),
),
DropdownMenuItem(
value: 'lifepo4',
child: Text(l10n.appSettings_batteryLifepo4),
),
DropdownMenuItem(
value: 'lipo',
child: Text(l10n.appSettings_batteryLipo),
),
],
),
],
),
),
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
isAdmin
? l10n.repeater_managementTools
: l10n.repeater_guestTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
@@ -210,26 +222,27 @@ class RepeaterHubScreen extends StatelessWidget {
);
},
),
const SizedBox(height: 12),
if (isAdmin) const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
if (isAdmin)
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
),
),
);
},
),
);
},
),
const SizedBox(height: 12),
// Neighbors button
_buildManagementCard(
@@ -248,26 +261,27 @@ class RepeaterHubScreen extends StatelessWidget {
);
},
),
const SizedBox(height: 12),
if (isAdmin) const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
if (isAdmin)
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
),
),
);
},
),
);
},
),
],
),
),
+56 -30
View File
@@ -8,7 +8,9 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_debug_log_service.dart';
import '../services/repeater_command_service.dart';
import '../services/storage_service.dart';
import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterSettingsScreen extends StatefulWidget {
final Contact repeater;
@@ -25,6 +27,8 @@ class RepeaterSettingsScreen extends StatefulWidget {
}
class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
final StorageService _storage = StorageService();
bool _isLoading = false;
bool _hasChanges = false;
bool _refreshingBasic = false;
@@ -59,6 +63,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
bool _repeatEnabled = true;
bool _allowReadOnly = true;
bool _privacyMode = false;
bool _autoClockSyncAfterLogin = false;
// Advertisement settings
bool _advertEnable = true;
@@ -464,18 +469,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (mounted) {
if (successCount > 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.repeater_refreshed(label)),
backgroundColor: Colors.green,
),
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_refreshed(label)),
backgroundColor: Colors.green,
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.repeater_errorRefreshing(label)),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_errorRefreshing(label)),
backgroundColor: Colors.red,
);
}
@@ -566,6 +569,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_lonController.text = widget.repeater.longitude?.toString() ?? '';
}
});
final autoClockSync = await _storage
.getRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
);
if (!mounted) return;
setState(() {
_autoClockSyncAfterLogin = autoClockSync;
});
}
Future<void> _saveSettings() async {
@@ -653,11 +665,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.repeater_settingsSaved),
backgroundColor: Colors.green,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.repeater_settingsSaved),
backgroundColor: Colors.green,
);
}
} catch (e) {
@@ -666,13 +677,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.repeater_errorSavingSettings(e.toString()),
),
backgroundColor: Colors.red,
showDismissibleSnackBar(
context,
content: Text(
context.l10n.repeater_errorSavingSettings(e.toString()),
),
backgroundColor: Colors.red,
);
}
}
@@ -1139,6 +1149,21 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
onRefresh: _refreshAllowReadOnly,
refreshTooltip: l10n.repeater_refreshGuestAccess,
),
SwitchListTile(
title: Text(l10n.repeater_clockSyncAfterLogin),
subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle),
value: _autoClockSyncAfterLogin,
onChanged: (value) async {
setState(() {
_autoClockSyncAfterLogin = value;
});
await _storage.setRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
value,
);
},
contentPadding: EdgeInsets.zero,
),
// Privacy mode - hidden until fully implemented
// _buildFeatureToggleRow(
// title: l10n.repeater_privacyMode,
@@ -1401,9 +1426,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (command == 'erase') {
if (mounted) {
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly)));
content: Text(l10n.repeater_eraseSerialOnly),
);
}
return;
}
@@ -1425,17 +1451,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
await connector.sendFrame(frame);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.repeater_commandSent(command))),
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_commandSent(command)),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
backgroundColor: Colors.red,
);
}
}
+9 -12
View File
@@ -12,6 +12,7 @@ import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../utils/battery_utils.dart';
import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterStatusScreen extends StatefulWidget {
final Contact repeater;
@@ -309,11 +310,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.repeater_statusRequestTimeout),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.repeater_statusRequestTimeout),
backgroundColor: Colors.red,
);
_recordStatusResult(false);
});
@@ -323,13 +323,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.repeater_errorLoadingStatus(e.toString()),
),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
backgroundColor: Colors.red,
);
}
_recordStatusResult(false);
+117 -9
View File
@@ -6,9 +6,11 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../services/linux_ble_error_classifier.dart';
import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart';
import 'tcp_screen.dart';
import 'usb_screen.dart';
@@ -288,23 +290,129 @@ class _ScannerScreenState extends State<ScannerScreen> {
MeshCoreConnector connector,
ScanResult result,
) async {
final name = result.device.platformName.isNotEmpty
? result.device.platformName
: result.advertisementData.advName;
try {
final name = result.device.platformName.isNotEmpty
? result.device.platformName
: result.advertisementData.advName;
await connector.connect(result.device, displayName: name);
await connector.connect(
result.device,
displayName: name,
linuxPairingPinProvider: PlatformInfo.isLinux
? () async {
if (!context.mounted) return null;
return _promptLinuxPairingPin(context, name);
}
: null,
);
} catch (e) {
final errorText = e.toString();
final suppressTransientLinuxConnectError =
PlatformInfo.isLinux &&
connector.isAutoReconnectScheduled &&
isLinuxBleConnectFailureText(errorText);
if (suppressTransientLinuxConnectError) {
appLogger.info(
'Suppressing transient Linux connect error while auto-reconnect is active: $e',
tag: 'ScannerScreen',
);
return;
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
backgroundColor: Colors.red,
);
}
}
}
Future<String?> _promptLinuxPairingPin(
BuildContext context,
String deviceName,
) async {
final l10n = context.l10n;
var pinValue = '';
var obscure = true;
appLogger.info(
'Showing Linux BLE pairing PIN prompt for $deviceName',
tag: 'ScannerScreen',
);
final pin = await showDialog<String>(
context: context,
builder: (dialogContext) {
return StatefulBuilder(
builder: (dialogContext, setDialogState) {
return AlertDialog(
title: Text(l10n.scanner_linuxPairingPinTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.scanner_linuxPairingPinPrompt(deviceName)),
const SizedBox(height: 12),
TextField(
autofocus: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
obscureText: obscure,
enableSuggestions: false,
autocorrect: false,
onChanged: (value) {
pinValue = value.trim();
},
onSubmitted: (value) {
Navigator.of(dialogContext).pop(value.trim());
},
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () {
setDialogState(() {
obscure = !obscure;
});
},
icon: Icon(
obscure ? Icons.visibility : Icons.visibility_off,
),
tooltip: obscure
? l10n.scanner_linuxPairingShowPin
: l10n.scanner_linuxPairingHidePin,
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(null),
child: Text(l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(pinValue),
child: Text(l10n.common_connect),
),
],
);
},
);
},
);
if (pin == null) {
appLogger.info(
'Linux BLE pairing PIN prompt cancelled for $deviceName',
tag: 'ScannerScreen',
);
return null;
}
appLogger.info(
'Linux BLE pairing PIN prompt completed for $deviceName',
tag: 'ScannerScreen',
);
return pin;
}
Widget _bluetoothOffWarning(BuildContext context) {
final errorColor = Theme.of(context).colorScheme.error;
return Container(
+469 -74
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meshcore_open/utils/gpx_export.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
@@ -8,10 +9,28 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/radio_settings.dart';
import '../services/app_debug_log_service.dart';
import '../widgets/app_bar.dart';
import '../helpers/snack_bar_builder.dart';
import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
import '../widgets/radio_stats_entry.dart';
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
/// to the UI enum range (always 5-8).
int _toUiCodingRate(int deviceCr) {
return deviceCr <= 4 ? deviceCr + 4 : deviceCr;
}
/// Convert UI coding-rate value (5-8) back to firmware encoding.
/// Uses the current device CR to detect which encoding the firmware expects.
int _toDeviceCodingRate(int uiCr, int? deviceCr) {
if (deviceCr != null && deviceCr <= 4) {
return uiCr - 4;
}
return uiCr;
}
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@@ -269,6 +288,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () => _showRadioSettings(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.sensors_outlined),
title: Text(l10n.radioStats_settingsTile),
subtitle: Text(l10n.radioStats_settingsSubtitle),
trailing: const Icon(Icons.chevron_right),
enabled:
connector.isConnected && connector.supportsCompanionRadioStats,
onTap: () => pushCompanionRadioStatsScreen(context),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.location_on_outlined),
title: Text(l10n.settings_location),
@@ -485,8 +514,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setNodeName(controller.text);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_nodeNameUpdated)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_nodeNameUpdated),
);
},
child: Text(l10n.common_save),
@@ -600,10 +630,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.settings_locationIntervalInvalid),
),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_locationIntervalInvalid),
);
return;
}
@@ -611,8 +640,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_locationUpdated),
);
}
@@ -632,15 +662,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
: currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_locationBothRequired),
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_locationInvalid),
);
return;
}
@@ -648,8 +680,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_locationUpdated),
);
},
child: Text(l10n.common_save),
@@ -663,9 +696,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.syncTime();
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
content: Text(l10n.settings_timeSynchronized),
);
}
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
@@ -730,23 +764,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return;
switch (result) {
case gpxExportSuccess:
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess)));
content: Text(l10n.settings_gpxExportSuccess),
);
case gpxExportNoContacts:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNoContacts)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_gpxExportNoContacts),
);
break;
case gpxExportNotAvailable:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_gpxExportNotAvailable),
);
break;
case gpxExportFailed:
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError)));
content: Text(l10n.settings_gpxExportError),
);
break;
}
}
@@ -973,6 +1011,15 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
},
),
const SizedBox(height: 8),
SwitchListTile(
title: Text(l10n.settings_multiAck),
value: multiAcks == 1,
onChanged: (value) {
setDialogState(() => multiAcks = value ? 1 : 0);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
initialValue: telemetryMode,
decoration: InputDecoration(
@@ -1014,21 +1061,6 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
}
},
),
const SizedBox(height: 16),
Text(
l10n.settings_multiAck(multiAcks.toString()),
style: Theme.of(context).textTheme.bodyMedium,
),
Slider(
value: multiAcks.toDouble(),
min: 0,
max: 2,
divisions: 2,
label: multiAcks.toString(),
onChanged: (value) {
setDialogState(() => multiAcks = value.round());
},
),
],
),
),
@@ -1049,8 +1081,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_telemetryModeUpdated),
);
},
child: Text(l10n.common_save),
@@ -1077,6 +1110,11 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
final _txPowerController = TextEditingController(text: '20');
bool _clientRepeat = false;
int? _selectedPresetIndex;
_RadioSettingsSnapshot? _lastNonRepeatSnapshot;
AppDebugLogService get _appLog =>
Provider.of<AppDebugLogService>(context, listen: false);
@override
void initState() {
@@ -1128,6 +1166,21 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
}
_clientRepeat = widget.connector.clientRepeat ?? false;
_selectedPresetIndex = _findMatchingPresetIndex();
if (_clientRepeat) {
_lastNonRepeatSnapshot =
_sessionRememberedNonRepeatSnapshot() ??
_inferNonRepeatSnapshotForRepeatEnabled();
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot!,
);
} else {
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_logRadioSettingsState('Dialog initialized');
});
}
@override
@@ -1137,14 +1190,223 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
super.dispose();
}
void _applyPreset(RadioSettings preset) {
void _applyPreset(int index) {
setState(() {
_frequencyController.text = preset.frequencyMHz.toString();
_bandwidth = preset.bandwidth;
_spreadingFactor = preset.spreadingFactor;
_codingRate = preset.codingRate;
_txPowerController.text = preset.txPowerDbm.toString();
_applyPresetState(index);
});
_logRadioSettingsState(
'Applied preset ${RadioSettings.presets[index].$1} (#$index)',
);
}
int? _findMatchingPresetIndex() {
return _findMatchingPresetIndexForSnapshot(_currentSnapshot());
}
int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) {
for (final i in _visiblePresetIndexes()) {
final preset = RadioSettings.presets[i].$2;
if (preset.frequencyHz == snapshot.frequencyHz &&
preset.bandwidth == snapshot.bandwidth &&
preset.spreadingFactor == snapshot.spreadingFactor &&
preset.codingRate == snapshot.codingRate &&
preset.txPowerDbm == snapshot.txPowerDbm) {
return i;
}
}
return null;
}
Iterable<int> _visiblePresetIndexes() sync* {
for (var i = 0; i < RadioSettings.presets.length; i++) {
if (_isOffGridPresetIndex(i)) {
continue;
}
yield i;
}
}
_RadioSettingsSnapshot _currentSnapshot() {
final frequencyMHz = double.tryParse(_frequencyController.text) ?? 915.0;
final txPowerDbm = int.tryParse(_txPowerController.text) ?? 20;
return _RadioSettingsSnapshot(
frequencyMHz: frequencyMHz,
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: txPowerDbm,
);
}
bool _isOffGridPresetIndex(int? index) {
if (index == null) return false;
return RadioSettings.presets[index].$1.startsWith('Off-Grid ');
}
double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) {
if (baseFrequencyMHz < 500) return 433.0;
if (baseFrequencyMHz < 900) return 869.0;
return 918.0;
}
double _normalFrequencyForBand(double frequencyMHz) {
if (frequencyMHz < 500) return 433.650;
if (frequencyMHz < 900) return 869.432;
return 915.8;
}
_RadioSettingsSnapshot _fallbackNonRepeatSnapshot(
double currentFrequencyMHz,
) {
return _RadioSettingsSnapshot(
frequencyMHz: _normalFrequencyForBand(currentFrequencyMHz),
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
);
}
_RadioSettingsSnapshot _nonRepeatSnapshotForCurrentSelection() {
final current = _currentSnapshot();
if (!_isOffGridPresetIndex(_selectedPresetIndex)) {
return current;
}
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
}
_RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() {
final snapshot = widget.connector.rememberedNonRepeatRadioState;
if (snapshot == null) return null;
return _RadioSettingsSnapshot.fromMeshCoreSnapshot(snapshot);
}
_RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() {
final current = _currentSnapshot();
for (final i in _visiblePresetIndexes()) {
final preset = RadioSettings.presets[i].$2;
final offGridFreqHz =
(_offGridFrequencyForBaseFrequency(preset.frequencyMHz) * 1000)
.round();
if (offGridFreqHz == current.frequencyHz &&
preset.bandwidth == current.bandwidth &&
preset.spreadingFactor == current.spreadingFactor &&
preset.codingRate == current.codingRate &&
preset.txPowerDbm == current.txPowerDbm) {
return _RadioSettingsSnapshot(
frequencyMHz: preset.frequencyMHz,
bandwidth: preset.bandwidth,
spreadingFactor: preset.spreadingFactor,
codingRate: preset.codingRate,
txPowerDbm: preset.txPowerDbm,
);
}
}
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
}
void _applySnapshot(_RadioSettingsSnapshot snapshot) {
_frequencyController.text = snapshot.frequencyMHz.toStringAsFixed(3);
_bandwidth = snapshot.bandwidth;
_spreadingFactor = snapshot.spreadingFactor;
_codingRate = snapshot.codingRate;
_txPowerController.text = snapshot.txPowerDbm.toString();
}
void _applyPresetState(int index) {
final preset = RadioSettings.presets[index].$2;
final baseSnapshot = _RadioSettingsSnapshot(
frequencyMHz: preset.frequencyMHz,
bandwidth: preset.bandwidth,
spreadingFactor: preset.spreadingFactor,
codingRate: preset.codingRate,
txPowerDbm: preset.txPowerDbm,
);
final frequencyMHz = _clientRepeat
? _offGridFrequencyForBaseFrequency(baseSnapshot.frequencyMHz)
: baseSnapshot.frequencyMHz;
_frequencyController.text = frequencyMHz.toString();
_bandwidth = preset.bandwidth;
_spreadingFactor = preset.spreadingFactor;
_codingRate = preset.codingRate;
_txPowerController.text = preset.txPowerDbm.toString();
_selectedPresetIndex = index;
_lastNonRepeatSnapshot = baseSnapshot;
}
void _syncPresetSelection() {
final previousPresetIndex = _selectedPresetIndex;
final previousLastNonRepeat = _lastNonRepeatSnapshot;
if (_clientRepeat) {
final baseSnapshot =
previousLastNonRepeat ?? _inferNonRepeatSnapshotForRepeatEnabled();
if (_bandwidth != baseSnapshot.bandwidth ||
_spreadingFactor != baseSnapshot.spreadingFactor ||
_codingRate != baseSnapshot.codingRate ||
(int.tryParse(_txPowerController.text) ?? 20) !=
baseSnapshot.txPowerDbm) {
_lastNonRepeatSnapshot = _RadioSettingsSnapshot(
frequencyMHz: baseSnapshot.frequencyMHz,
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
);
}
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot ?? baseSnapshot,
);
if (previousPresetIndex != _selectedPresetIndex ||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
_logRadioSettingsState(
'Preset match updated while repeat enabled: ${_presetLabel(previousPresetIndex)} -> ${_presetLabel(_selectedPresetIndex)}',
);
}
return;
}
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot!,
);
if (previousPresetIndex != _selectedPresetIndex ||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
_logRadioSettingsState(
'Preset sync updated state from ${_presetLabel(previousPresetIndex)} to ${_presetLabel(_selectedPresetIndex)}',
);
}
}
void _handleManualSettingsChanged(String source) {
_logRadioSettingsState('Manual settings edit: $source');
setState(_syncPresetSelection);
}
void _handleClientRepeatChanged(bool enabled) {
_logRadioSettingsState(
'Off-grid repeat toggle requested: $_clientRepeat -> $enabled',
);
setState(() {
final currentSnapshot = _currentSnapshot();
if (enabled) {
if (!_clientRepeat) {
_syncPresetSelection();
}
final baseSnapshot = _lastNonRepeatSnapshot ?? currentSnapshot;
_clientRepeat = true;
_frequencyController.text = _offGridFrequencyForBaseFrequency(
baseSnapshot.frequencyMHz,
).toStringAsFixed(3);
return;
}
_clientRepeat = false;
_applySnapshot(
_lastNonRepeatSnapshot ??
_fallbackNonRepeatSnapshot(currentSnapshot.frequencyMHz),
);
_syncPresetSelection();
});
_logRadioSettingsState('Off-grid repeat toggle applied');
}
Future<void> _saveSettings() async {
@@ -1153,18 +1415,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final txPower = int.tryParse(_txPowerController.text);
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
ScaffoldMessenger.of(
showDismissibleSnackBar(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
content: Text(l10n.settings_frequencyInvalid),
);
return;
}
final maxTxPower = widget.connector.maxTxPower ?? 22;
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
),
showDismissibleSnackBar(
context,
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
);
return;
}
@@ -1184,14 +1446,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (knownRepeat) {
const validRepeatFreqsKHz = {433000, 869000, 918000};
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_clientRepeatFreqWarning),
);
return;
}
}
try {
_logRadioSettingsState('Saving radio settings');
await widget.connector.sendFrame(
buildSetRadioParamsFrame(
freqHz,
@@ -1203,29 +1467,64 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
final rememberedSnapshot = _clientRepeat
? _lastNonRepeatSnapshot
: _currentSnapshot();
if (rememberedSnapshot != null) {
widget.connector.rememberNonRepeatRadioState(
rememberedSnapshot.toMeshCoreSnapshot(widget.connector.currentCr),
);
}
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_radioSettingsUpdated)),
_logRadioSettingsState('Radio settings saved successfully');
showDismissibleSnackBar(
context,
content: Text(l10n.settings_radioSettingsUpdated),
);
} catch (e) {
_appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_error(e.toString()))),
showDismissibleSnackBar(
context,
content: Text(l10n.settings_error(e.toString())),
);
}
Navigator.pop(context);
}
int _toUiCodingRate(int deviceCr) {
return deviceCr <= 4 ? deviceCr + 4 : deviceCr;
}
int _toDeviceCodingRate(int uiCr, int? deviceCr) {
if (deviceCr != null && deviceCr <= 4) {
return uiCr - 4;
String _presetLabel(int? index) {
if (index == null) {
return 'custom';
}
return uiCr;
return '${RadioSettings.presets[index].$1} (#$index)';
}
String _formatSnapshot(_RadioSettingsSnapshot? snapshot) {
if (snapshot == null) {
return 'null';
}
return '${snapshot.frequencyMHz.toStringAsFixed(3)}MHz/'
'${snapshot.bandwidth.label}/'
'${snapshot.spreadingFactor.label}/'
'${snapshot.codingRate.label}/'
'${snapshot.txPowerDbm}dBm';
}
void _logRadioSettingsState(String message) {
if (!kDebugMode) return;
_appLog.info(
'$message | '
'freq=${_frequencyController.text}MHz '
'bw=${_bandwidth.label} '
'sf=${_spreadingFactor.label} '
'cr=${_codingRate.label} '
'tx=${_txPowerController.text}dBm '
'repeat=$_clientRepeat '
'preset=${_presetLabel(_selectedPresetIndex)} '
'lastNonRepeat=${_formatSnapshot(_lastNonRepeatSnapshot)}',
tag: 'RadioSettings',
);
}
@override
@@ -1239,12 +1538,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<int>(
key: ValueKey<int?>(_selectedPresetIndex),
initialValue: _selectedPresetIndex,
decoration: InputDecoration(
labelText: l10n.settings_presets,
border: const OutlineInputBorder(),
),
items: [
for (var i = 0; i < RadioSettings.presets.length; i++)
for (final i in _visiblePresetIndexes())
DropdownMenuItem(
value: i,
child: Text(RadioSettings.presets[i].$1),
@@ -1252,13 +1553,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
],
onChanged: (index) {
if (index != null) {
_applyPreset(RadioSettings.presets[index].$2);
_applyPreset(index);
}
},
),
const SizedBox(height: 16),
TextField(
controller: _frequencyController,
onChanged: (_) => _handleManualSettingsChanged('frequency'),
decoration: InputDecoration(
labelText: l10n.settings_frequency,
border: const OutlineInputBorder(),
@@ -1281,7 +1583,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _bandwidth = value);
if (value != null) {
setState(() {
_bandwidth = value;
_syncPresetSelection();
});
_logRadioSettingsState('Manual settings edit: bandwidth');
}
},
),
const SizedBox(height: 16),
@@ -1297,7 +1605,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _spreadingFactor = value);
if (value != null) {
setState(() {
_spreadingFactor = value;
_syncPresetSelection();
});
_logRadioSettingsState(
'Manual settings edit: spreading factor',
);
}
},
),
const SizedBox(height: 16),
@@ -1313,12 +1629,19 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _codingRate = value);
if (value != null) {
setState(() {
_codingRate = value;
_syncPresetSelection();
});
_logRadioSettingsState('Manual settings edit: coding rate');
}
},
),
const SizedBox(height: 16),
TextField(
controller: _txPowerController,
onChanged: (_) => _handleManualSettingsChanged('tx power'),
decoration: InputDecoration(
labelText: l10n.settings_txPower,
border: const OutlineInputBorder(),
@@ -1334,7 +1657,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
title: Text(l10n.settings_clientRepeat),
subtitle: Text(l10n.settings_clientRepeatSubtitle),
value: _clientRepeat,
onChanged: (value) => setState(() => _clientRepeat = value),
onChanged: _handleClientRepeatChanged,
contentPadding: EdgeInsets.zero,
),
],
@@ -1351,3 +1674,75 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
);
}
}
class _RadioSettingsSnapshot {
final double frequencyMHz;
final LoRaBandwidth bandwidth;
final LoRaSpreadingFactor spreadingFactor;
final LoRaCodingRate codingRate;
final int txPowerDbm;
const _RadioSettingsSnapshot({
required this.frequencyMHz,
required this.bandwidth,
required this.spreadingFactor,
required this.codingRate,
required this.txPowerDbm,
});
/// Frequency in integer Hz avoids floating-point comparison issues.
int get frequencyHz => (frequencyMHz * 1000).round();
/// Convert from the connector's raw-int snapshot to UI-enum snapshot.
static _RadioSettingsSnapshot? fromMeshCoreSnapshot(
MeshCoreRadioStateSnapshot snapshot,
) {
final bw = LoRaBandwidth.values
.where((b) => b.hz == snapshot.bwHz)
.firstOrNull;
final sf = LoRaSpreadingFactor.values
.where((s) => s.value == snapshot.sf)
.firstOrNull;
final cr = LoRaCodingRate.values
.where((c) => c.value == _toUiCodingRate(snapshot.cr))
.firstOrNull;
if (bw == null || sf == null || cr == null) return null;
return _RadioSettingsSnapshot(
frequencyMHz: snapshot.freqHz / 1000.0,
bandwidth: bw,
spreadingFactor: sf,
codingRate: cr,
txPowerDbm: snapshot.txPowerDbm,
);
}
/// Convert back to the connector's raw-int snapshot.
MeshCoreRadioStateSnapshot toMeshCoreSnapshot(int? deviceCr) {
return MeshCoreRadioStateSnapshot(
freqHz: frequencyHz,
bwHz: bandwidth.hz,
sf: spreadingFactor.value,
cr: _toDeviceCodingRate(codingRate.value, deviceCr),
txPowerDbm: txPowerDbm,
);
}
@override
bool operator ==(Object other) {
return other is _RadioSettingsSnapshot &&
frequencyHz == other.frequencyHz &&
bandwidth == other.bandwidth &&
spreadingFactor == other.spreadingFactor &&
codingRate == other.codingRate &&
txPowerDbm == other.txPowerDbm;
}
@override
int get hashCode => Object.hash(
frequencyHz,
bandwidth,
spreadingFactor,
codingRate,
txPowerDbm,
);
}
+5 -2
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../utils/platform_info.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart';
import 'usb_screen.dart';
@@ -270,8 +271,10 @@ class _TcpScreenState extends State<TcpScreen> {
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
showDismissibleSnackBar(
context,
content: Text(message),
backgroundColor: Colors.red,
);
}
+13 -15
View File
@@ -14,6 +14,7 @@ import '../utils/app_logger.dart';
import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart';
import '../helpers/snack_bar_builder.dart';
class TelemetryScreen extends StatefulWidget {
final Contact contact;
@@ -86,11 +87,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
);
_recordTelemetryResult(false);
});
@@ -137,11 +137,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_parsedTelemetry = parsedTelemetry;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.telemetry_receivedData),
backgroundColor: Colors.green,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_receivedData),
backgroundColor: Colors.green,
);
_statusTimeout?.cancel();
if (!mounted) return;
@@ -182,11 +181,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
backgroundColor: Colors.red,
);
}
}
+5 -5
View File
@@ -10,6 +10,7 @@ import '../utils/app_logger.dart';
import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart';
import 'scanner_screen.dart';
import 'tcp_screen.dart';
@@ -383,11 +384,10 @@ class _UsbScreenState extends State<UsbScreen> {
void _showError(Object error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_friendlyErrorMessage(error)),
backgroundColor: Colors.red,
),
showDismissibleSnackBar(
context,
content: Text(_friendlyErrorMessage(error)),
backgroundColor: Colors.red,
);
}
+35
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../storage/prefs_manager.dart';
import '../utils/app_logger.dart';
@@ -218,4 +219,38 @@ class AppSettingsService extends ChangeNotifier {
Future<void> setTcpServerPort(int value) async {
await updateSettings(_settings.copyWith(tcpServerPort: value));
}
Future<void> setJumpToOldestUnread(bool value) async {
await updateSettings(_settings.copyWith(jumpToOldestUnread: value));
}
Future<void> setTranslationEnabled(bool value) async {
await updateSettings(_settings.copyWith(translationEnabled: value));
}
Future<void> setTranslationTargetLanguageCode(String? value) async {
await updateSettings(
_settings.copyWith(translationTargetLanguageCode: value),
);
}
Future<void> setComposerTranslationEnabled(bool value) async {
await updateSettings(_settings.copyWith(composerTranslationEnabled: value));
}
Future<void> setTranslationModelSourceUrl(String? value) async {
await updateSettings(_settings.copyWith(translationModelSourceUrl: value));
}
Future<void> setTranslationSelectedModelId(String? value) async {
await updateSettings(_settings.copyWith(translationSelectedModelId: value));
}
Future<void> setTranslationDownloadedModels(
List<TranslationModelRecord> value,
) async {
await updateSettings(
_settings.copyWith(translationDownloadedModels: value),
);
}
}
@@ -0,0 +1,37 @@
const String linuxConnectStageFailureMarker = 'linux connect stage failure';
bool isLinuxBleConnectFailureText(String errorText) {
final lowerErrorText = errorText.toLowerCase();
if (isLinuxBlePairingFailureText(errorText)) {
return false;
}
return lowerErrorText.contains(linuxConnectStageFailureMarker) ||
lowerErrorText.contains('| connect |') ||
lowerErrorText.contains('linux connect hard-timeout') ||
lowerErrorText.contains('org.bluez.error.failed') ||
lowerErrorText.contains('org.bluez.error.inprogress') ||
lowerErrorText.contains('le-connection-abort-by-local');
}
bool isLinuxBlePairingFailureText(String errorText) {
final lowerErrorText = errorText.toLowerCase();
final isPairingSpecificStateError =
lowerErrorText.contains('bad state: no element') &&
(lowerErrorText.contains('pair') ||
lowerErrorText.contains('bond') ||
lowerErrorText.contains('trust'));
return lowerErrorText.contains('authenticationfailed') ||
lowerErrorText.contains('authentication failed') ||
lowerErrorText.contains('notpermitted: not paired') ||
lowerErrorText.contains('pairing fallback failed') ||
lowerErrorText.contains('linux ble pairing did not complete') ||
lowerErrorText.contains('linux ble trust repair did not complete') ||
isPairingSpecificStateError ||
isLikelyLinuxBlePairingTimeoutText(errorText);
}
bool isLikelyLinuxBlePairingTimeoutText(String errorText) {
final lowerErrorText = errorText.toLowerCase();
return lowerErrorText.contains('timed out') &&
(lowerErrorText.contains('pair') || lowerErrorText.contains('bond'));
}
+423
View File
@@ -0,0 +1,423 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
typedef ProcessStartFn =
Future<Process> Function(String executable, List<String> arguments);
typedef ProcessRunFn =
Future<ProcessResult> Function(String executable, List<String> arguments);
/// Best-effort Linux BLE pairing helper using bluetoothctl.
///
/// This is used only as a fallback when BlueZ pairing via flutter_blue_plus
/// fails to surface agent prompts in-app.
class LinuxBlePairingService {
/// Maximum number of pairing attempts (initial + retries).
/// Covers one remove-and-retry plus one proactive-PIN retry.
static const int _maxAttempts = 3;
static const Duration _processExitTimeout = Duration(seconds: 6);
static const Duration _pairingCleanupTimeout = Duration(seconds: 5);
static const Duration _defaultPairingTimeout = Duration(seconds: 45);
LinuxBlePairingService({
ProcessStartFn? processStart,
ProcessRunFn? processRun,
}) : _processStart = processStart ?? Process.start,
_processRun = processRun ?? Process.run;
final ProcessStartFn _processStart;
final ProcessRunFn _processRun;
Future<bool> isBluetoothctlAvailable() async {
try {
final result = await _processRun('bluetoothctl', <String>['--version']);
return result.exitCode == 0;
} on ProcessException {
return false;
}
}
Future<void> disconnectDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {
onLog?.call('Requesting BlueZ disconnect for $remoteId');
Process process;
try {
process = await _processStart('bluetoothctl', <String>[]);
} on ProcessException catch (error) {
onLog?.call(
'bluetoothctl unavailable, skipping BlueZ disconnect: $error',
);
return;
}
process.stdin.writeln('disconnect $remoteId');
process.stdin.writeln('quit');
try {
await process.exitCode.timeout(_processExitTimeout);
} catch (_) {
process.kill();
}
onLog?.call('Issued bluetoothctl disconnect for $remoteId');
}
Future<bool> isPairedAndTrusted(String remoteId) async {
ProcessResult result;
try {
result = await _processRun('bluetoothctl', <String>['info', remoteId]);
} on ProcessException {
return false;
}
if (result.exitCode != 0) {
return false;
}
final output = (result.stdout as String).toLowerCase();
return output.contains('paired: yes') && output.contains('trusted: yes');
}
Future<bool> trustDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {
onLog?.call('Requesting BlueZ trust for $remoteId');
ProcessResult result;
try {
result = await _processRun('bluetoothctl', <String>['trust', remoteId]);
} on ProcessException catch (error) {
onLog?.call('bluetoothctl unavailable, cannot trust $remoteId: $error');
return false;
}
if (result.exitCode != 0) {
onLog?.call('bluetoothctl trust failed for $remoteId: ${result.stderr}');
return false;
}
final trusted = await isPairedAndTrusted(remoteId);
onLog?.call(
trusted
? 'Verified BlueZ trust for $remoteId'
: 'BlueZ trust verification failed for $remoteId',
);
return trusted;
}
Future<bool> pairAndTrust({
required String remoteId,
Duration timeout = _defaultPairingTimeout,
void Function(String message)? onLog,
Future<String?> Function()? onRequestPin,
}) async {
var removeRetryUsed = false;
var proactivePinRetryUsed = false;
Future<String?> Function()? currentPinProvider = onRequestPin;
for (var attempt = 0; attempt < _maxAttempts; attempt++) {
final result = await _runPairingAttempt(
remoteId: remoteId,
timeout: timeout,
onLog: onLog,
onRequestPin: currentPinProvider,
);
if (result.success) return true;
if (result.userCancelled) {
onLog?.call('Pairing cancelled by user; skipping retry/remove flow');
return false;
}
if (result.pairFailed) {
if (!removeRetryUsed) {
removeRetryUsed = true;
onLog?.call(
'Pairing failed; removing cached bond and retrying '
'(attempt ${attempt + 1}/$_maxAttempts)',
);
await _removeDevice(remoteId, onLog: onLog);
continue;
}
if (!result.pinSent &&
!proactivePinRetryUsed &&
currentPinProvider != null) {
proactivePinRetryUsed = true;
onLog?.call(
'Pairing failed before PIN challenge; requesting PIN for '
'proactive retry (attempt ${attempt + 1}/$_maxAttempts)',
);
final pin = await currentPinProvider();
if (pin == null) {
onLog?.call('PIN entry cancelled for proactive retry');
return false;
}
final capturedPin = pin.trim();
currentPinProvider = () async => capturedPin;
continue;
}
return false;
}
// Timeout path pairing neither succeeded nor failed.
onLog?.call('Pairing did not complete before timeout');
if (!result.pinSent &&
!proactivePinRetryUsed &&
currentPinProvider != null) {
proactivePinRetryUsed = true;
onLog?.call(
'No PIN challenge observed before timeout; requesting PIN for '
'proactive retry (attempt ${attempt + 1}/$_maxAttempts)',
);
final pin = await currentPinProvider();
if (pin == null) {
onLog?.call('PIN entry cancelled for proactive retry after timeout');
return false;
}
final capturedPin = pin.trim();
currentPinProvider = () async => capturedPin;
continue;
}
return false;
}
return false;
}
/// Runs a single bluetoothctl pairing attempt.
///
/// Uses a [Completer] to wake as soon as pairing succeeds or fails,
/// instead of polling.
Future<_PairingResult> _runPairingAttempt({
required String remoteId,
required Duration timeout,
void Function(String message)? onLog,
Future<String?> Function()? onRequestPin,
}) async {
onLog?.call('Starting bluetoothctl pairing flow for $remoteId');
Process process;
try {
process = await _processStart('bluetoothctl', <String>[]);
} on ProcessException catch (error) {
onLog?.call('bluetoothctl unavailable, cannot run pairing flow: $error');
return const _PairingResult();
}
final output = StringBuffer();
var pinSent = false;
var sessionClosed = false;
var userCancelledPinEntry = false;
var confirmationHandled = false;
var successHandled = false;
var failureHandled = false;
var detectorBuffer = '';
final pairingDone = Completer<void>();
var pairSucceeded = false;
var pairFailed = false;
void writeCmd(String cmd) {
if (sessionClosed) return;
try {
process.stdin.writeln(cmd);
} on StateError {
sessionClosed = true;
onLog?.call('bluetoothctl stdin already closed; ignoring "$cmd"');
}
}
unawaited(
process.exitCode.then((_) {
sessionClosed = true;
if (!pairingDone.isCompleted) pairingDone.complete();
}),
);
void handleChunk(String chunk) {
output.write(chunk);
detectorBuffer += chunk.toLowerCase();
if (detectorBuffer.length > 4096) {
detectorBuffer = detectorBuffer.substring(detectorBuffer.length - 4096);
}
final lower = detectorBuffer;
if (!pinSent &&
!sessionClosed &&
(lower.contains('enter pin code') ||
lower.contains('requestpin') ||
lower.contains('input pin code') ||
lower.contains('request passkey') ||
lower.contains('requestpasskey') ||
lower.contains('enter passkey'))) {
pinSent = true;
if (onRequestPin == null) {
onLog?.call(
'PIN/passkey requested but no onRequestPin callback; '
'sending empty line to accept default pairing',
);
writeCmd('');
} else {
onLog?.call('Pairing agent is ready for PIN/passkey input');
unawaited(
Future<void>(() async {
String? pin;
try {
pin = await onRequestPin();
} catch (e) {
onLog?.call('onRequestPin callback threw: $e');
pairFailed = true;
writeCmd('cancel');
if (!pairingDone.isCompleted) pairingDone.complete();
return;
}
if (pin == null) {
if (sessionClosed) {
onLog?.call(
'PIN prompt resolved after pairing session closed',
);
return;
}
onLog?.call('PIN entry cancelled by user; cancelling pairing');
userCancelledPinEntry = true;
pairFailed = true;
writeCmd('cancel');
if (!pairingDone.isCompleted) pairingDone.complete();
return;
}
if (sessionClosed) {
onLog?.call(
'PIN provided after pairing session closed; ignoring',
);
return;
}
if (pin.trim().isEmpty) {
onLog?.call(
'Blank PIN submitted; sending empty line to accept default pairing',
);
writeCmd('');
} else {
onLog?.call('Submitting PIN/passkey to pairing agent');
writeCmd(pin.trim());
}
}),
);
}
}
if (!confirmationHandled &&
(lower.contains('confirm passkey') ||
lower.contains('requestconfirmation') ||
lower.contains('[agent] confirm'))) {
confirmationHandled = true;
onLog?.call(
'Pairing agent requested passkey confirmation; answering yes',
);
writeCmd('yes');
}
if (!successHandled &&
(lower.contains('pairing successful') ||
lower.contains('already paired'))) {
successHandled = true;
onLog?.call('Pairing reported success');
pairSucceeded = true;
if (!pairingDone.isCompleted) pairingDone.complete();
}
if (!failureHandled &&
(lower.contains('failed to pair') ||
lower.contains('authenticationfailed') ||
lower.contains('authentication failed'))) {
failureHandled = true;
onLog?.call('Pairing reported authentication failure');
pairFailed = true;
if (!pairingDone.isCompleted) pairingDone.complete();
}
}
final stdoutSub = process.stdout
.transform(utf8.decoder)
.listen(handleChunk);
final stderrSub = process.stderr
.transform(utf8.decoder)
.listen(handleChunk);
writeCmd('power on');
writeCmd('agent KeyboardDisplay');
writeCmd('default-agent');
onLog?.call('Waiting for pairing challenge from bluetoothctl agent');
writeCmd('pair $remoteId');
// Wait for the Completer to fire (success/failure/process exit) or timeout.
await pairingDone.future.timeout(timeout, onTimeout: () {});
if (!pairFailed && pairSucceeded) {
onLog?.call('Pair succeeded; trusting and connecting device');
writeCmd('trust $remoteId');
writeCmd('connect $remoteId');
}
writeCmd('quit');
sessionClosed = true;
try {
await process.exitCode.timeout(_pairingCleanupTimeout);
} catch (_) {
process.kill();
}
await stdoutSub.cancel();
await stderrSub.cancel();
if (pairFailed) {
return _PairingResult(
pairFailed: true,
pinSent: pinSent,
userCancelled: userCancelledPinEntry,
);
}
final allOutput = output.toString().toLowerCase();
final reportedSuccess =
pairSucceeded ||
allOutput.contains('pairing successful') ||
allOutput.contains('already paired');
if (reportedSuccess) {
final trusted = await trustDevice(remoteId, onLog: onLog);
if (!trusted) {
onLog?.call('Pairing completed but BlueZ trust was not restored');
}
return _PairingResult(success: trusted, pinSent: pinSent);
}
return _PairingResult(pinSent: pinSent);
}
Future<void> _removeDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {
Process process;
try {
process = await _processStart('bluetoothctl', <String>[]);
} on ProcessException catch (error) {
onLog?.call(
'bluetoothctl unavailable, skipping remove for $remoteId: $error',
);
return;
}
process.stdin.writeln('remove $remoteId');
process.stdin.writeln('quit');
try {
await process.exitCode.timeout(_processExitTimeout);
} catch (_) {
process.kill();
}
onLog?.call('Issued bluetoothctl remove for $remoteId');
}
}
/// Outcome of a single bluetoothctl pairing attempt.
class _PairingResult {
final bool success;
final bool pairFailed;
final bool pinSent;
final bool userCancelled;
const _PairingResult({
this.success = false,
this.pairFailed = false,
this.pinSent = false,
this.userCancelled = false,
});
}
@@ -0,0 +1,28 @@
/// No-op stub for web builds where dart:io is unavailable.
///
/// The real implementation lives in linux_ble_pairing_service.dart and is
/// selected via conditional import in meshcore_connector.dart.
class LinuxBlePairingService {
LinuxBlePairingService();
Future<bool> isBluetoothctlAvailable() async => false;
Future<void> disconnectDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {}
Future<bool> isPairedAndTrusted(String remoteId) async => false;
Future<bool> trustDevice(
String remoteId, {
void Function(String message)? onLog,
}) async => false;
Future<bool> pairAndTrust({
required String remoteId,
Duration timeout = const Duration(seconds: 45),
void Function(String message)? onLog,
Future<String?> Function()? onRequestPin,
}) async => false;
}
+23 -8
View File
@@ -21,11 +21,16 @@ class _AckHistoryEntry {
});
}
/// (messageId, timestamp, attemptIndex) stored per ACK hash for O(1) lookup.
/// (messageId, timestamp, attemptIndex, pathSelection) stored per ACK hash
/// for O(1) lookup. [pathSelection] snapshots the route used for this
/// specific attempt so that a late PUSH_CODE_SEND_CONFIRMED credits the
/// correct path even when the message has since been retried on a different
/// route.
typedef AckHashMapping = ({
String messageId,
DateTime timestamp,
int attemptIndex,
PathSelection? pathSelection,
});
class RetryServiceConfig {
@@ -133,6 +138,9 @@ class MessageRetryService extends ChangeNotifier {
Future<void> sendMessageWithRetry({
required Contact contact,
required String text,
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
Uint8List? pathBytes,
int? pathLength,
}) async {
@@ -145,6 +153,9 @@ class MessageRetryService extends ChangeNotifier {
final message = Message(
senderKey: contact.publicKey,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
status: MessageStatus.pending,
@@ -382,6 +393,7 @@ class MessageRetryService extends ChangeNotifier {
messageId: messageId,
timestamp: DateTime.now(),
attemptIndex: message.retryCount,
pathSelection: _selectionFromMessage(message),
);
// Add this ACK hash to the list of expected ACKs for this message (for history)
@@ -395,14 +407,11 @@ class MessageRetryService extends ChangeNotifier {
int actualTimeout = timeoutMs;
if (config.calculateTimeout != null) {
final calculated = config.calculateTimeout!(
actualTimeout = config.calculateTimeout!(
pathLengthValue,
message.text.length,
contactKey: contact.publicKeyHex,
);
if (timeoutMs <= 0 || calculated < timeoutMs) {
actualTimeout = calculated;
}
}
final updatedMessage = message.copyWith(
@@ -569,6 +578,7 @@ class MessageRetryService extends ChangeNotifier {
final config = _config;
String? matchedMessageId;
int? matchedAttemptIndex;
PathSelection? matchedPathSelection;
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
// Clean up old ACK hash mappings (older than 15 minutes)
@@ -588,6 +598,7 @@ class MessageRetryService extends ChangeNotifier {
if (mapping != null) {
matchedMessageId = mapping.messageId;
matchedAttemptIndex = mapping.attemptIndex;
matchedPathSelection = mapping.pathSelection;
} else {
config?.debugLogService?.warn(
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex not found in direct mapping, trying fallback',
@@ -618,13 +629,13 @@ class MessageRetryService extends ChangeNotifier {
}
final contact = _pendingContacts[matchedMessageId];
final ackedAttempt = matchedAttemptIndex ?? message.retryCount;
final selection = _selectionFromMessage(message);
final selection = matchedPathSelection ?? _selectionFromMessage(message);
final shortText = message.text.length > 20
? '${message.text.substring(0, 20)}...'
: message.text;
config?.debugLogService?.info(
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on retry ${ackedAttempt + 1} in ${tripTimeMs}ms',
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on attempt $ackedAttempt in ${tripTimeMs}ms',
tag: 'AckHash',
);
@@ -636,6 +647,8 @@ class MessageRetryService extends ChangeNotifier {
tripTimeMs: tripTimeMs,
);
final wasAlreadyResolved = _resolvedMessages.contains(matchedMessageId);
_cleanupMessage(matchedMessageId);
config?.updateMessage(deliveredMessage);
@@ -658,7 +671,9 @@ class MessageRetryService extends ChangeNotifier {
tripTimeMs,
);
}
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
if (!wasAlreadyResolved) {
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
}
}
notifyListeners();
+34
View File
@@ -7,8 +7,42 @@ class StorageService {
static const String _pathHistoryPrefix = 'path_history_';
static const String _pendingMessagesKey = 'pending_messages';
static const String _repeaterPasswordsKey = 'repeater_passwords';
static const String _repeaterAutoClockSyncAfterLoginKey =
'repeater_auto_clock_sync_after_login';
static const String _deliveryObservationsKey = 'delivery_observations';
Future<Map<String, bool>> _loadRepeaterAutoClockSyncAfterLogin() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey);
if (jsonStr == null) return {};
try {
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
return json.map((key, value) => MapEntry(key, value == true));
} catch (e) {
return {};
}
}
Future<bool> getRepeaterAutoClockSyncAfterLoginEnabled(
String repeaterPubKeyHex,
) async {
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
return settings[repeaterPubKeyHex] ?? false;
}
Future<void> setRepeaterAutoClockSyncAfterLoginEnabled(
String repeaterPubKeyHex,
bool enabled,
) async {
final prefs = PrefsManager.instance;
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
settings[repeaterPubKeyHex] = enabled;
final jsonStr = jsonEncode(settings);
await prefs.setString(_repeaterAutoClockSyncAfterLoginKey, jsonStr);
}
Future<void> savePathHistory(
String contactPubKeyHex,
ContactPathHistory history,
+2
View File
@@ -0,0 +1,2 @@
export 'translation_file_store_stub.dart'
if (dart.library.io) 'translation_file_store_io.dart';
+131
View File
@@ -0,0 +1,131 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../models/translation_support.dart';
class TranslationFileStore {
Future<String> modelDirectoryPath() async {
final baseDir = await getApplicationDocumentsDirectory();
final dir = Directory('${baseDir.path}/translation_models');
if (!dir.existsSync()) {
await dir.create(recursive: true);
}
return dir.path;
}
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
final dir = Directory(await modelDirectoryPath());
if (!dir.existsSync()) {
return const [];
}
final models = <TranslationModelRecord>[];
for (final entity in dir.listSync().whereType<File>()) {
final name = entity.uri.pathSegments.last;
// Skip hidden chunk files from interrupted parallel downloads.
if (name.startsWith('.')) {
await entity.delete();
continue;
}
final stat = entity.statSync();
models.add(
TranslationModelRecord(
id: name,
name: name,
sourceUrl: '',
localPath: entity.path,
downloadedAt: stat.modified,
fileSizeBytes: stat.size,
),
);
}
return models;
}
Future<void> deleteModel(TranslationModelRecord model) async {
await deleteFile(model.localPath);
}
Future<void> deleteFile(String path) async {
final file = File(path);
if (file.existsSync()) {
await file.delete();
}
}
Future<DownloadedModelFile> writeModelBytes({
required String fileName,
required Stream<List<int>> chunks,
}) async {
final directoryPath = await modelDirectoryPath();
final file = File('$directoryPath/$fileName');
final sink = file.openWrite();
var fileSizeBytes = 0;
var completed = false;
try {
await for (final chunk in chunks) {
sink.add(chunk);
fileSizeBytes += chunk.length;
}
completed = true;
} finally {
await sink.close();
if (!completed && file.existsSync()) {
await file.delete();
}
}
return DownloadedModelFile(
localPath: file.path,
fileSizeBytes: fileSizeBytes,
);
}
Future<String> chunkFilePath(String fileName, int index) async {
final dir = await modelDirectoryPath();
return '$dir/.${fileName}_chunk_$index';
}
Future<DownloadedModelFile> combineChunks({
required String fileName,
required List<String> chunkPaths,
}) async {
final dir = await modelDirectoryPath();
final finalPath = '$dir/$fileName';
final sink = File(finalPath).openWrite();
var totalSize = 0;
var completed = false;
try {
for (final chunkPath in chunkPaths) {
final chunkFile = File(chunkPath);
await sink.addStream(chunkFile.openRead());
totalSize += await chunkFile.length();
}
completed = true;
} finally {
await sink.close();
for (final chunkPath in chunkPaths) {
final file = File(chunkPath);
if (file.existsSync()) {
await file.delete();
}
}
if (!completed) {
final finalFile = File(finalPath);
if (finalFile.existsSync()) {
await finalFile.delete();
}
}
}
return DownloadedModelFile(localPath: finalPath, fileSizeBytes: totalSize);
}
}
class DownloadedModelFile {
final String localPath;
final int fileSizeBytes;
const DownloadedModelFile({
required this.localPath,
required this.fileSizeBytes,
});
}
@@ -0,0 +1,43 @@
import '../models/translation_support.dart';
class TranslationFileStore {
Future<String> modelDirectoryPath() async {
throw UnsupportedError('Local model storage is not supported on web.');
}
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
return const [];
}
Future<void> deleteModel(TranslationModelRecord model) async {}
Future<void> deleteFile(String path) async {}
Future<DownloadedModelFile> writeModelBytes({
required String fileName,
required Stream<List<int>> chunks,
}) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
Future<String> chunkFilePath(String fileName, int index) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
Future<DownloadedModelFile> combineChunks({
required String fileName,
required List<String> chunkPaths,
}) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
}
class DownloadedModelFile {
final String localPath;
final int fileSizeBytes;
const DownloadedModelFile({
required this.localPath,
required this.fileSizeBytes,
});
}
+663
View File
@@ -0,0 +1,663 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:llamadart/llamadart.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../helpers/gif_helper.dart';
import '../utils/app_logger.dart';
import 'app_settings_service.dart';
import 'translation_file_store.dart';
class TranslationResult {
final String translatedText;
final String targetLanguageCode;
final String? detectedLanguageCode;
final String? modelId;
final MessageTranslationStatus status;
const TranslationResult({
required this.translatedText,
required this.targetLanguageCode,
required this.status,
this.detectedLanguageCode,
this.modelId,
});
}
class TranslationDownloadCancelled implements Exception {
const TranslationDownloadCancelled();
@override
String toString() => 'Download canceled.';
}
class TranslationService extends ChangeNotifier {
final AppSettingsService _appSettingsService;
final TranslationFileStore _fileStore;
TranslationService(
this._appSettingsService, {
TranslationFileStore? fileStore,
}) : _fileStore = fileStore ?? TranslationFileStore();
bool _isBusy = false;
bool _isDownloading = false;
bool _cancelDownloadRequested = false;
String? _lastError;
Future<void> _queue = Future<void>.value();
LlamaEngine? _engine;
String? _loadedModelPath;
String? _failedModelPath;
int _downloadedBytes = 0;
int? _downloadTotalBytes;
String? _downloadFileName;
bool get isBusy => _isBusy;
bool get isDownloading => _isDownloading;
String? get lastError => _lastError;
int get downloadedBytes => _downloadedBytes;
int? get downloadTotalBytes => _downloadTotalBytes;
String? get downloadFileName => _downloadFileName;
double? get downloadProgress {
final total = _downloadTotalBytes;
if (!_isDownloading || total == null || total <= 0) {
return null;
}
return (_downloadedBytes / total).clamp(0.0, 1.0);
}
AppSettings get _settings => _appSettingsService.settings;
String? resolvedTargetLanguageCode(String? fallbackLanguageCode) {
return _settings.translationTargetLanguageCode ??
_settings.languageOverride ??
fallbackLanguageCode;
}
String? resolvedIncomingLanguageCode(String? fallbackLanguageCode) {
return _settings.translationTargetLanguageCode ??
_settings.languageOverride ??
fallbackLanguageCode ??
'en';
}
bool shouldTranslateIncoming({
required String text,
required bool isCli,
required bool isOutgoing,
}) {
if (!_settings.translationEnabled || isCli || isOutgoing) {
return false;
}
return _isPlainTextEligible(text);
}
bool shouldTranslateOutgoing({
required String text,
required String? targetLanguageCode,
}) {
return _settings.composerTranslationEnabled &&
targetLanguageCode != null &&
targetLanguageCode.isNotEmpty &&
_isPlainTextEligible(text);
}
List<TranslationModelRecord> get availableModels =>
_settings.translationDownloadedModels;
TranslationModelRecord? get selectedModel {
final selectedId = _settings.translationSelectedModelId;
if (selectedId == null) {
return availableModels.isNotEmpty ? availableModels.first : null;
}
for (final model in availableModels) {
if (model.id == selectedId) {
return model;
}
}
return availableModels.isNotEmpty ? availableModels.first : null;
}
Future<void> refreshDownloadedModels() async {
if (_isDownloading) return;
final scanned = await _fileStore.scanDownloadedModels();
if (scanned.isEmpty) {
return;
}
final existingByPath = {
for (final model in _settings.translationDownloadedModels)
model.localPath: model,
};
final merged = scanned.map((model) {
final existing = existingByPath[model.localPath];
if (existing == null) {
return model;
}
return TranslationModelRecord(
id: existing.id,
name: existing.name,
sourceUrl: existing.sourceUrl,
localPath: existing.localPath,
downloadedAt: existing.downloadedAt,
fileSizeBytes: model.fileSizeBytes,
);
}).toList();
await _appSettingsService.setTranslationDownloadedModels(merged);
_failedModelPath = null;
if (_settings.translationSelectedModelId == null && merged.isNotEmpty) {
await _appSettingsService.setTranslationSelectedModelId(merged.first.id);
}
}
static const int _parallelChunks = 8;
static const int _parallelMinBytes = 10 * 1024 * 1024; // 10 MB
Future<TranslationModelRecord> downloadModel({
required String sourceUrl,
String? fileName,
String? id,
}) async {
final uri = Uri.tryParse(sourceUrl);
if (uri == null || !uri.hasScheme) {
throw ArgumentError('Invalid model URL.');
}
return _runExclusive(() async {
_setBusy(true);
_setDownloading(true);
_lastError = null;
try {
final resolvedFileName =
fileName ??
_sanitizeFileName(
uri.pathSegments.isNotEmpty
? uri.pathSegments.last
: 'translation-model.gguf',
);
_downloadFileName = resolvedFileName;
_downloadedBytes = 0;
_cancelDownloadRequested = false;
// HEAD request to check size and range support.
final headClient = http.Client();
int? totalSize;
bool supportsRange = false;
try {
final headResponse = await headClient.send(http.Request('HEAD', uri));
totalSize = headResponse.contentLength;
supportsRange =
headResponse.headers['accept-ranges']?.contains('bytes') == true;
await headResponse.stream.drain<void>();
} finally {
headClient.close();
}
_downloadTotalBytes = totalSize;
notifyListeners();
DownloadedModelFile downloaded;
if (supportsRange &&
totalSize != null &&
totalSize > _parallelMinBytes) {
downloaded = await _downloadParallel(
uri: uri,
fileName: resolvedFileName,
totalSize: totalSize,
);
} else {
downloaded = await _downloadSingle(
uri: uri,
fileName: resolvedFileName,
);
}
final record = TranslationModelRecord(
id: id ?? resolvedFileName,
name: resolvedFileName,
sourceUrl: sourceUrl,
localPath: downloaded.localPath,
downloadedAt: DateTime.now(),
fileSizeBytes: downloaded.fileSizeBytes,
);
final updated = [
for (final existing in _settings.translationDownloadedModels)
if (existing.id != record.id) existing,
record,
];
await _appSettingsService.setTranslationDownloadedModels(updated);
await _appSettingsService.setTranslationSelectedModelId(record.id);
await _appSettingsService.setTranslationModelSourceUrl(sourceUrl);
_failedModelPath = null;
return record;
} finally {
_setDownloading(false);
}
});
}
Future<DownloadedModelFile> _downloadSingle({
required Uri uri,
required String fileName,
}) async {
final client = http.Client();
try {
final response = await client.send(http.Request('GET', uri));
if (response.statusCode < 200 || response.statusCode >= 300) {
throw StateError('Model download failed: HTTP ${response.statusCode}');
}
_downloadTotalBytes ??= response.contentLength;
notifyListeners();
final trackedStream = _trackDownloadProgress(response.stream);
return await _fileStore.writeModelBytes(
fileName: fileName,
chunks: trackedStream,
);
} finally {
client.close();
}
}
Future<DownloadedModelFile> _downloadParallel({
required Uri uri,
required String fileName,
required int totalSize,
}) async {
final chunkSize = (totalSize / _parallelChunks).ceil();
final chunkPaths = <String>[];
final clients = <http.Client>[];
var combineReached = false;
try {
final futures = <Future<void>>[];
for (var i = 0; i < _parallelChunks; i++) {
final start = i * chunkSize;
final end = (start + chunkSize - 1).clamp(0, totalSize - 1);
if (start >= totalSize) break;
final chunkPath = await _fileStore.chunkFilePath(fileName, i);
chunkPaths.add(chunkPath);
final client = http.Client();
clients.add(client);
futures.add(
_downloadRange(
client: client,
uri: uri,
chunkPath: chunkPath,
start: start,
end: end,
),
);
}
await Future.wait(futures);
if (_cancelDownloadRequested) {
throw const TranslationDownloadCancelled();
}
_downloadFileName = 'Merging chunks...';
notifyListeners();
combineReached = true;
return await _fileStore.combineChunks(
fileName: fileName,
chunkPaths: chunkPaths,
);
} finally {
for (final client in clients) {
client.close();
}
if (!combineReached) {
for (final chunkPath in chunkPaths) {
await _fileStore.deleteFile(chunkPath);
}
}
}
}
Future<void> _downloadRange({
required http.Client client,
required Uri uri,
required String chunkPath,
required int start,
required int end,
}) async {
final request = http.Request('GET', uri);
request.headers['Range'] = 'bytes=$start-$end';
final response = await client.send(request);
if (response.statusCode != 206) {
await response.stream.drain<void>();
throw StateError(
'Range download failed: HTTP ${response.statusCode}'
'${response.statusCode == 200 ? ' (server ignored Range header)' : ''}',
);
}
final trackedStream = _trackDownloadProgress(response.stream);
await _fileStore.writeModelBytes(
fileName: chunkPath.split(RegExp(r'[/\\]')).last,
chunks: trackedStream,
);
}
void cancelDownload() {
if (!_isDownloading) {
return;
}
_cancelDownloadRequested = true;
_lastError = 'Download stopped.';
notifyListeners();
}
Future<void> removeModel(TranslationModelRecord model) async {
await _runExclusive(() async {
_setBusy(true);
_lastError = null;
await _fileStore.deleteModel(model);
final updated = _settings.translationDownloadedModels
.where((entry) => entry.id != model.id)
.toList();
await _appSettingsService.setTranslationDownloadedModels(updated);
if (_settings.translationSelectedModelId == model.id) {
await _appSettingsService.setTranslationSelectedModelId(
updated.isNotEmpty ? updated.first.id : null,
);
}
});
}
Future<TranslationResult?> translateIncomingText({
required String text,
required String? targetLanguageCode,
}) async {
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
return null;
}
final detectedLanguageCode = await detectLanguage(text);
if (detectedLanguageCode != null &&
detectedLanguageCode == targetLanguageCode) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
final translatedText = await _translateText(
text: text,
targetLanguageCode: targetLanguageCode,
sourceLanguageCode: detectedLanguageCode,
);
if (translatedText == null || translatedText.trim().isEmpty) {
return null;
}
// If translation is nearly identical, text was already in target language.
if (translatedText.trim().toLowerCase() == text.trim().toLowerCase()) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
return TranslationResult(
translatedText: translatedText.trim(),
targetLanguageCode: targetLanguageCode,
detectedLanguageCode: detectedLanguageCode,
modelId: selectedModel?.id,
status: MessageTranslationStatus.completed,
);
}
Future<TranslationResult?> translateOutgoingText({
required String text,
required String? targetLanguageCode,
}) async {
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
return null;
}
final detectedLanguageCode = await detectLanguage(text);
if (detectedLanguageCode != null &&
detectedLanguageCode == targetLanguageCode) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
final translatedText = await _translateText(
text: text,
targetLanguageCode: targetLanguageCode,
sourceLanguageCode: detectedLanguageCode,
);
if (translatedText == null || translatedText.trim().isEmpty) {
return null;
}
return TranslationResult(
translatedText: translatedText.trim(),
targetLanguageCode: targetLanguageCode,
detectedLanguageCode: detectedLanguageCode,
modelId: selectedModel?.id,
status: MessageTranslationStatus.completed,
);
}
Future<String?> detectLanguage(String text) async {
return _heuristicLanguageCode(text);
}
Future<String?> _translateText({
required String text,
required String targetLanguageCode,
String? sourceLanguageCode,
}) async {
if (!_hasUsableModel) {
return null;
}
final model = selectedModel;
if (model == null || model.localPath.isEmpty) {
return null;
}
final targetLabel = _languageLabel(targetLanguageCode);
final instruction = targetLanguageCode == 'zh'
? '将以下文本翻译为中文,注意只需要输出翻译后的结果,不要额外解释:\n\n$text'
: 'Translate the following segment into $targetLabel, without additional explanation.\n\n$text';
try {
return await _runExclusive(() async {
final engine = await _ensureContext(model.localPath);
if (engine == null) {
return null;
}
final messages = [
LlamaChatMessage.fromText(
role: LlamaChatRole.user,
text: instruction,
),
];
final output = StringBuffer();
await for (final chunk in engine.create(
messages,
params: const GenerationParams(
maxTokens: 256,
temp: 0.7,
topK: 20,
topP: 0.6,
penalty: 1.05,
reusePromptPrefix: false,
),
enableThinking: false,
sourceLangCode: sourceLanguageCode,
targetLangCode: targetLanguageCode,
)) {
final content = chunk.choices.firstOrNull?.delta.content;
if (content != null) {
output.write(content);
}
if (output.length >= text.length * 4 + 100) {
break;
}
}
return _sanitizeOutput(output.toString());
});
} catch (error) {
_lastError = error.toString();
appLogger.warn('Translation request failed: $error');
notifyListeners();
return null;
}
}
bool get _hasUsableModel {
final model = selectedModel;
return !kIsWeb && model != null && model.localPath.isNotEmpty;
}
bool _isPlainTextEligible(String text) {
final trimmed = text.trim();
if (trimmed.isEmpty) {
return false;
}
if (GifHelper.parseGif(trimmed) != null) {
return false;
}
return !(trimmed.startsWith('m:') ||
trimmed.startsWith('V1|') ||
trimmed.startsWith('r:'));
}
String? _heuristicLanguageCode(String text) {
if (RegExp(r'[іїєґІЇЄҐ]').hasMatch(text)) {
return 'uk';
}
if (RegExp(r'[а-яёА-ЯЁ]').hasMatch(text)) {
return 'ru';
}
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
return 'ja';
}
if (RegExp(r'[가-힣]').hasMatch(text)) {
return 'ko';
}
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
return 'zh';
}
// Latin-script languages can't be reliably distinguished by characters
// alone return null so the translator always attempts translation.
return null;
}
String _languageLabel(String code) {
for (final option in supportedTranslationLanguages) {
if (option.code == code) {
return option.label;
}
}
return code.toUpperCase();
}
String _sanitizeOutput(String raw) {
var result = raw.trim();
result = result.replaceAll(RegExp(r'\*\*'), '');
result = result.replaceAll(RegExp(r'<[^>]+>'), '');
return result.trim();
}
String _sanitizeFileName(String fileName) {
final cleaned = fileName.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
return cleaned.isEmpty ? 'translation-model.gguf' : cleaned;
}
Future<LlamaEngine?> _ensureContext(String modelPath) async {
if (_engine != null && _loadedModelPath == modelPath) {
return _engine;
}
if (modelPath == _failedModelPath) {
return null;
}
if (_engine != null) {
await _engine!.dispose();
_engine = null;
_loadedModelPath = null;
}
final engine = LlamaEngine(LlamaBackend());
try {
await engine.loadModel(
modelPath,
modelParams: const ModelParams(
gpuLayers: 0,
preferredBackend: GpuBackend.cpu,
),
);
_engine = engine;
_loadedModelPath = modelPath;
_failedModelPath = null;
return _engine;
} catch (_) {
await engine.dispose();
_failedModelPath = modelPath;
rethrow;
}
}
Future<void> releaseModel() async {
await _runExclusive(() async {
final engine = _engine;
if (engine == null) {
_loadedModelPath = null;
return;
}
_engine = null;
_loadedModelPath = null;
await engine.dispose();
});
}
Future<T> _runExclusive<T>(Future<T> Function() action) {
final completer = Completer<T>();
_setBusy(true);
_queue = _queue.then((_) async {
try {
completer.complete(await action());
} catch (error, stackTrace) {
completer.completeError(error, stackTrace);
} finally {
_setBusy(false);
}
});
return completer.future;
}
Stream<List<int>> _trackDownloadProgress(Stream<List<int>> source) async* {
await for (final chunk in source) {
if (_cancelDownloadRequested) {
throw const TranslationDownloadCancelled();
}
_downloadedBytes += chunk.length;
notifyListeners();
yield chunk;
}
}
void _setBusy(bool value) {
if (_isBusy == value) {
return;
}
_isBusy = value;
notifyListeners();
}
void _setDownloading(bool value) {
_isDownloading = value;
if (!value) {
_cancelDownloadRequested = false;
_downloadedBytes = 0;
_downloadTotalBytes = null;
_downloadFileName = null;
}
notifyListeners();
}
@override
void dispose() {
final engine = _engine;
_engine = null;
_loadedModelPath = null;
if (engine != null) {
unawaited(engine.dispose());
}
super.dispose();
}
}
+11 -11
View File
@@ -273,7 +273,7 @@ class UsbSerialService {
throw StateError('USB serial port is not open');
}
final packet = wrapUsbSerialTxFrame(data);
_logFrameSummary('USB TX frame', data);
// _logFrameSummary('USB TX frame', data);
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('write', {
@@ -447,16 +447,16 @@ class UsbSerialService {
await _frameController.close();
}
void _logFrameSummary(String prefix, Uint8List bytes) {
if (bytes.isEmpty) {
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
return;
}
_debugLogService?.info(
'$prefix code=${bytes[0]} len=${bytes.length}',
tag: 'USB Serial',
);
}
// void _logFrameSummary(String prefix, Uint8List bytes) {
// if (bytes.isEmpty) {
// _debugLogService?.info('$prefix len=0', tag: 'USB Serial');
// return;
// }
// _debugLogService?.info(
// '$prefix code=${bytes[0]} len=${bytes.length}',
// tag: 'USB Serial',
// );
// }
/// Returns an ordered list of port paths to try for [portName].
///
+13
View File
@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../models/channel_message.dart';
import '../models/translation_support.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
@@ -98,6 +99,11 @@ class ChannelMessageStore {
'senderKey': msg.senderKey != null ? base64Encode(msg.senderKey!) : null,
'senderName': msg.senderName,
'text': msg.text,
'originalText': msg.originalText,
'translatedText': msg.translatedText,
'translatedLanguageCode': msg.translatedLanguageCode,
'translationStatus': msg.translationStatus.value,
'translationModelId': msg.translationModelId,
'timestamp': msg.timestamp.millisecondsSinceEpoch,
'isOutgoing': msg.isOutgoing,
'status': msg.status.index,
@@ -126,6 +132,13 @@ class ChannelMessageStore {
: null,
senderName: json['senderName'] as String,
text: decodedText,
originalText: json['originalText'] as String?,
translatedText: json['translatedText'] as String?,
translatedLanguageCode: json['translatedLanguageCode'] as String?,
translationStatus: parseMessageTranslationStatus(
json['translationStatus'],
),
translationModelId: json['translationModelId'] as String?,
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
isOutgoing: json['isOutgoing'] as bool,
status: ChannelMessageStatus.values[json['status'] as int],
+13
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import '../models/message.dart';
import '../models/translation_support.dart';
import '../helpers/smaz.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
@@ -83,6 +84,11 @@ class MessageStore {
'isCli': msg.isCli,
'status': msg.status.index,
'messageId': msg.messageId,
'originalText': msg.originalText,
'translatedText': msg.translatedText,
'translatedLanguageCode': msg.translatedLanguageCode,
'translationStatus': msg.translationStatus.value,
'translationModelId': msg.translationModelId,
'retryCount': msg.retryCount,
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
'expectedAckHash': msg.expectedAckHash,
@@ -115,6 +121,13 @@ class MessageStore {
isCli: isCli,
status: MessageStatus.values[json['status'] as int],
messageId: json['messageId'] as String?,
originalText: json['originalText'] as String?,
translatedText: json['translatedText'] as String?,
translatedLanguageCode: json['translatedLanguageCode'] as String?,
translationStatus: parseMessageTranslationStatus(
json['translationStatus'],
),
translationModelId: json['translationModelId'] as String?,
retryCount: json['retryCount'] as int? ?? 0,
estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?,
expectedAckHash: json['expectedAckHash'] as int? ?? 0,
+25 -9
View File
@@ -14,12 +14,13 @@ class ContactExport {
final double lon;
final String desc;
final double? ele;
final String url;
ContactExport({
required this.name,
required this.lat,
required this.lon,
required this.desc,
required this.url,
this.ele,
});
}
@@ -40,6 +41,7 @@ class GpxExport {
String name,
double lat,
double lon,
String url,
String desc, [
double? ele,
]) {
@@ -50,55 +52,66 @@ class GpxExport {
lon: lon,
desc: desc.trim(),
ele: ele,
url: url,
),
);
}
void addRepeaters() {
final contacts = _connector.contacts
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
.toList();
final contacts = _connector.allContacts.where(
(c) => c.type == advTypeRepeater || c.type == advTypeRoom,
);
for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) {
continue;
}
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact(
contact.name,
contact.latitude!,
contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
);
}
}
void addContacts() {
final contacts = _connector.contacts
.where((c) => c.type == advTypeChat)
.toList();
final contacts = _connector.allContacts.where((c) => c.type == advTypeChat);
for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) {
continue;
}
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact(
contact.name,
contact.latitude!,
contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
);
}
}
void addAll() {
final contacts = _connector.contacts;
for (var contact in contacts.toList()) {
final contacts = _connector.allContacts;
for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) {
continue;
}
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact(
contact.name,
contact.latitude ?? 0.0,
contact.longitude ?? 0.0,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
);
}
}
@@ -138,6 +151,9 @@ class GpxExport {
ele: c.ele,
name: c.name,
desc: c.desc,
extensions: {
"meshcore": {"url": c.url},
},
),
)
.toList();
+7 -2
View File
@@ -3,6 +3,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/widgets/battery_indicator.dart';
import 'package:provider/provider.dart';
import 'radio_stats_entry.dart';
import 'snr_indicator.dart';
class AppBarTitle extends StatelessWidget {
@@ -10,12 +11,14 @@ class AppBarTitle extends StatelessWidget {
final Widget? leading;
final Widget? trailing;
final bool indicators;
final bool showBatteryIndicator;
final bool subtitle;
const AppBarTitle(
this.title, {
this.leading,
this.trailing,
this.indicators = true,
this.showBatteryIndicator = true,
this.subtitle = true,
super.key,
});
@@ -33,7 +36,7 @@ class AppBarTitle extends StatelessWidget {
final compact = availableWidth < 170;
final showSubtitle =
!compact && connector.isConnected && selfName != null && subtitle;
final showBattery = availableWidth >= 60;
final showBattery = showBatteryIndicator && availableWidth >= 60;
final showSnr = availableWidth >= 110;
final showIndicators = (showBattery || showSnr) && indicators;
@@ -60,11 +63,13 @@ class AppBarTitle extends StatelessWidget {
if (showIndicators) const SizedBox(width: 6),
if (showIndicators)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (showBattery) BatteryIndicator(connector: connector),
if (showSnr) SNRIndicator(connector: connector),
if (connector.supportsCompanionRadioStats)
const RadioStatsIconButton(compact: true),
],
),
trailing ?? const SizedBox.shrink(),
+137
View File
@@ -0,0 +1,137 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../helpers/utf8_length_limiter.dart';
/// A [TextField] that displays a live UTF-8 byte counter.
///
/// The counter appears below the field once the user starts typing and changes
/// colour as the limit is approached (orange at 70 %, error-red at 90 %).
///
/// All standard [TextField] behaviour (focus nodes, input actions, decoration
/// overrides, etc.) is forwarded so the widget can be dropped into any screen.
class ByteCountedTextField extends StatelessWidget {
/// Maximum number of UTF-8 bytes allowed.
final int maxBytes;
/// Controller for the text field.
final TextEditingController controller;
/// Optional focus node forwarded to the inner [TextField].
final FocusNode? focusNode;
/// Hint text shown when the field is empty.
final String? hintText;
/// Keyboard action button (defaults to [TextInputAction.send]).
final TextInputAction textInputAction;
/// Called when the user submits via the keyboard action button.
final ValueChanged<String>? onSubmitted;
/// Additional [TextInputFormatter]s applied *before* the byte limiter.
final List<TextInputFormatter> extraFormatters;
/// Text capitalisation forwarded to the inner [TextField].
final TextCapitalization textCapitalization;
/// Optional full [InputDecoration] override. When provided, [hintText] is
/// ignored set it inside the decoration instead.
final InputDecoration? decoration;
/// Ratio (01) at which the counter turns the warning colour (default 0.7).
final double warningThreshold;
/// Ratio (01) at which the counter turns the error colour (default 0.9).
final double errorThreshold;
/// Whether to hide the counter when the field is empty (default `true`).
final bool hideCounterWhenEmpty;
/// Optional encoder function to transform text before byte counting/limiting.
/// If provided, byte limits and counters will use the encoded text length.
final String Function(String)? encoder;
const ByteCountedTextField({
super.key,
required this.maxBytes,
required this.controller,
this.focusNode,
this.hintText,
this.textInputAction = TextInputAction.send,
this.onSubmitted,
this.extraFormatters = const [],
this.textCapitalization = TextCapitalization.sentences,
this.decoration,
this.warningThreshold = 0.7,
this.errorThreshold = 0.9,
this.hideCounterWhenEmpty = true,
this.encoder,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) {
final effectiveText = encoder != null
? encoder!(value.text)
: value.text;
final usedBytes = utf8.encode(effectiveText).length;
final ratio = maxBytes > 0 ? usedBytes / maxBytes : 0.0;
final showCounter = !(hideCounterWhenEmpty && value.text.isEmpty);
final counterColor = ratio > errorThreshold
? Theme.of(context).colorScheme.error
: ratio > warningThreshold
? Colors.orange
: Theme.of(context).colorScheme.onSurfaceVariant;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
maxLines: null,
controller: controller,
focusNode: focusNode,
inputFormatters: [
...extraFormatters,
Utf8LengthLimitingTextInputFormatter(
maxBytes,
encoder: encoder,
),
],
textCapitalization: textCapitalization,
decoration:
decoration ??
InputDecoration(
hintText: hintText,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
textInputAction: textInputAction,
onSubmitted: onSubmitted,
),
if (showCounter)
Padding(
padding: const EdgeInsets.only(top: 4, right: 4),
child: Align(
alignment: Alignment.centerRight,
child: Text(
'$usedBytes / $maxBytes',
style: TextStyle(fontSize: 11, color: counterColor),
),
),
),
],
);
},
);
}
}

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