Compare commits

..

239 Commits

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

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

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

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

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

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

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

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

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

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

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

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

* Update lib/screens/map_screen.dart

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

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

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

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

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

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

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

* Update lib/connector/meshcore_connector.dart

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

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

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

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

---------

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

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

* Remove unused app_logger import from ContactDiscoveryStore

* Add warning log for empty publicKeyHex in saveChannelMessages method

* Add warning log for empty publicKeyHex in clearMessages method

* Migrate legacy storage keys to scoped keys across multiple stores

* Remove legacy unscoped keys during migration in storage classes

* Update lib/storage/contact_store.dart

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

---------

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

* Update lib/connector/meshcore_connector.dart

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

* Fix pointer tracking in BufferReader's readCString method

---------

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

* Refactor _parseContactMessage to improve error handling and message parsing logic

* Update lib/connector/meshcore_connector.dart

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

---------

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

After

Width:  |  Height:  |  Size: 104 KiB

+30
View File
@@ -0,0 +1,30 @@
# MeshCore Open - Feature Documentation
MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does.
## Table of Contents
1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection
2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation
3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing
4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries
5. [Channels](channels.md) - Broadcast channels, communities, and channel chat
6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching
7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
## App Overview
MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can:
- **Chat** with other mesh nodes via encrypted direct messages
- **Broadcast** on shared channels (public, hashtag, private, or community-scoped)
- **View nodes on a map** with GPS locations, predicted positions, and path traces
- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings
- **Share contacts** via `meshcore://` URIs and QR codes
- **Configure radio settings** including frequency, power, bandwidth, and spreading factor
- **Cache offline maps** for use without internet connectivity
- **Analyze line-of-sight** between nodes with terrain elevation profiles
+187
View File
@@ -0,0 +1,187 @@
# Additional Features
## GIF Picker (Giphy Integration)
### How to Access
In any chat screen (direct or channel), tap the GIF button in the message input bar.
### What the User Sees
A bottom sheet with a search field and a grid of GIF thumbnails.
### Key Interactions
- On open, loads trending GIFs (G-rated, 25 results)
- Type to search and press the keyboard submit button (search triggers on submit, not on each keystroke). Clearing the search field reloads trending GIFs
- On network/API errors, a "Retry" button is shown in-place
- Tap a GIF to select it — the chat input shows an inline preview with an X button to dismiss
- Send the message to transmit the GIF reference (`g:<giphy-id>`)
- Recipients see the GIF rendered inline via Giphy CDN
- "Powered by Giphy" attribution is always shown at the bottom of the picker
- The bottom sheet occupies 70% of screen height
---
## Localization / Multi-Language Support
### How to Access
App Settings → Appearance → Language
### Supported Languages (15)
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian
### How It Works
- All UI strings go through Flutter's ARB localization system
- Language can follow the system locale or be explicitly overridden
- Changes take effect immediately
---
## Discovered Contacts Screen
### How to Access
From Contacts screen → overflow menu → "Discovered Contacts"
### What the User Sees
A list of nodes heard passively over the air but not yet added as contacts. Each shows:
- Color-coded avatar (by type)
- Name
- Short public key
- Last-seen time
### Key Interactions
- Search bar with debounced filtering
- Sort by last seen or name; filter by type
- **Tap**: Import the contact (adds to your contact list)
- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete
- Overflow menu → "Delete All" (with confirmation)
- Already-known contacts and your own node are filtered out
---
## SMAZ Compression
### What It Is
An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text).
### How to Enable
- **Per contact**: Chat screen → info button → toggle "SMAZ compression"
- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression"
### How It Works
- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed
- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data
- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text
- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages
- Disabled by default
---
## Community QR Scanner
### How to Access
From Channels screen → "+" FAB → "Scan Community QR"
### What the User Sees
A live QR scanner view with instruction text overlay.
### Key Interactions
- Scan a community QR code shared by another member
- On valid scan: confirmation dialog showing community name and ID
- Option to "Add public channel to device" on join
- If already a member: shows an "Already a member" dialog
- Invalid QR: shows an orange error snackbar
---
## Channel Message Path Viewing
### How to Access
In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop).
### What the User Sees
- Summary card: sender, time, repeat count, path type, observed hops
- "Other Observed Paths" section (if multiple paths detected)
- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates
### Actions
- **Radar icon**: Opens path trace map for live trace
- **Map icon**: Opens a map with hop markers and polyline
- **Path dropdown**: Switch between observed path variants (if multiple)
---
## Debug Logging
### BLE Debug Log
**Access**: Settings → BLE Debug Log
Two views:
- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex.
- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary.
### App Debug Log
**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug)
Structured log entries with level (Info/Warning/Error), tag, message, and timestamp.
Both logs support copy-all and clear operations.
---
## Chrome Required Screen
### When It Appears
Automatically shown on web platforms when a non-Chromium browser is detected.
### What the User Sees
A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational.
---
## Path History Service
### What It Does (Background Service)
Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking:
- Hop count and trip time
- Success/failure counts and route weights
- Flood vs. direct discovery
### Path Scoring
Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path.
Used internally for:
- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths
- **Path selection**: Picks the best-scored path for each retry attempt
- **Flood statistics**: Tracks flood vs. direct discovery ratios
---
## Message Retry Service
### What It Does (Background Service)
Handles reliable delivery of outgoing direct messages:
1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued
2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]`
3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...)
4. Each retry may use a different path (via path history diversity window)
5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path
6. Reports RTT and path data for quality learning
7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs
### Configurable Settings (App Settings → Messaging)
- Max retries (210, default 5)
- Clear path on max retry (on/off)
- Auto route rotation with weight parameters
---
## Timeout Prediction (ML)
### What It Does (Background Service)
An ML-based service that predicts expected delivery timeouts:
- Collects delivery observations (path length, message size, time since last RX, delivery time) in a sliding window of up to 100 observations (oldest evicted first)
- Requires **10 minimum observations** before first training. After that, retrains every 5 new observations
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
- Features with zero variance are automatically excluded from training
- Blends per-contact statistics with ML predictions
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
+249
View File
@@ -0,0 +1,249 @@
# BLE Protocol & Data Layer
This is a technical reference for the communication protocol and data architecture.
## Transport Layer
The app supports three transports, all sharing the same command/response protocol:
| Transport | Method | Implementation |
|---|---|---|
| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` |
| USB Serial | Packet-framed serial | `MeshCoreUsbManager` |
| TCP | Packet-framed socket | `MeshCoreTcpConnector` |
### BLE (Nordic UART Service)
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response".
### USB and TCP Framing
Both use a lightweight packet framing codec:
```
TX (host → device): [0x3C][len_lo][len_hi][payload...]
RX (device → host): [0x3E][len_lo][len_hi][payload...]
```
- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming
- Length: 2-byte little-endian, payload only
- Max payload: 172 bytes
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
- USB: 10ms post-write delay between frames
## Connection State Machine
```
enum MeshCoreConnectionState {
disconnected,
scanning,
connecting,
connected,
disconnecting,
}
```
## BLE Connection Lifecycle
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
2. **Connect** with 15-second timeout
3. **Request MTU** 185 bytes (non-web only)
4. **Discover services** and locate NUS
5. **Enable TX notifications** (up to 3 attempts on native)
6. **Subscribe** to TX characteristic for incoming frames
7. **Initial sync**: device info query, time sync, channel sync
## Auto-Reconnect (BLE Only)
On unexpected disconnection, auto-reconnect with exponential backoff:
- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s...
- Resets on successful connection
- Disabled for manual disconnects
- Not available for USB or TCP
## Protocol Constants
| Constant | Value | Description |
|---|---|---|
| Max frame size | 172 bytes | BLE/USB/TCP payload limit |
| Public key size | 32 bytes | Ed25519 public key |
| Max path size | 64 bytes | Maximum path data |
| Max name size | 32 bytes | Maximum node name |
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
| App protocol version | 3 | Sent in device query |
| Contact frame size | 148 bytes | Fixed-size contact record |
## Command Codes (App → Device)
| Code | Name | Description |
|------|------|-------------|
| 1 | CMD_APP_START | Announce app connection |
| 2 | CMD_SEND_TXT_MSG | Send direct text message |
| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message |
| 4 | CMD_GET_CONTACTS | Request contact list |
| 5 | CMD_GET_DEVICE_TIME | Query device clock |
| 6 | CMD_SET_DEVICE_TIME | Set device clock |
| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement |
| 8 | CMD_SET_ADVERT_NAME | Set node name |
| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact |
| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message |
| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters |
| 12 | CMD_SET_RADIO_TX_POWER | Set TX power |
| 13 | CMD_RESET_PATH | Reset contact path |
| 14 | CMD_SET_ADVERT_LATLON | Set advertised location |
| 15 | CMD_REMOVE_CONTACT | Remove a contact |
| 16 | CMD_SHARE_CONTACT | Share contact to mesh |
| 17 | CMD_EXPORT_CONTACT | Export contact as bytes |
| 18 | CMD_IMPORT_CONTACT | Import contact from bytes |
| 19 | CMD_REBOOT | Reboot device |
| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage |
| 22 | CMD_DEVICE_QUERY | Query device info |
| 26 | CMD_SEND_LOGIN | Login to repeater/room |
| 27 | CMD_SEND_STATUS_REQ | Request repeater status |
| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key |
| 31 | CMD_GET_CHANNEL | Get channel definition |
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
## Response / Push Codes (Device → App)
| Code | Name | Description |
|------|------|-------------|
| 0 | RESP_CODE_OK | Generic success |
| 1 | RESP_CODE_ERR | Generic error |
| 2 | RESP_CODE_CONTACTS_START | Contact list begins |
| 3 | RESP_CODE_CONTACT | Single contact data |
| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete |
| 5 | RESP_CODE_SELF_INFO | Device self-info response |
| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [25]=ack_hash, [69]=estimated_timeout_ms` |
| 7 | RESP_CODE_CONTACT_MSG_RECV | Incoming direct message (v2) |
| 8 | RESP_CODE_CHANNEL_MSG_RECV | Incoming channel message (v2) |
| 10 | RESP_CODE_NO_MORE_MESSAGES | No more queued messages |
| 11 | RESP_CODE_EXPORT_CONTACT | Exported contact data |
| 9 | RESP_CODE_CURR_TIME | Current device time |
| 12 | RESP_CODE_BATT_AND_STORAGE | Battery mV (uint16 LE) + storage used/total (uint32 LE each) |
| 13 | RESP_CODE_DEVICE_INFO | Firmware info |
| 16 | RESP_CODE_CONTACT_MSG_RECV_V3 | Incoming direct message (v3) |
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) |
| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued |
| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded |
| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed |
| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response |
| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet |
| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result |
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
## Data Models
### Contact
32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout:
```
[0] = resp_code
[132] = public key (32 bytes)
[33] = type (1=chat, 2=repeater, 3=room, 4=sensor)
[34] = flags (bit 0 = favorite)
[35] = path_length
[3699] = path (64 bytes)
[100131] = name (32 bytes, null-padded)
[132135] = timestamp (uint32 LE)
[136139] = latitude (int32 LE, × 1e-6 degrees)
[140143] = longitude (int32 LE, × 1e-6 degrees)
[144147] = last_modified (uint32 LE)
```
### Message (Direct)
Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions.
### Channel Message
Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields.
### Channel
Index (07), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels.
### Community
UUID, name, 32-byte secret, hashtag channel list. Shared via QR code.
## Persistence
All data is stored via `SharedPreferences` (JSON-serialized). No SQLite or other database.
| Data | Storage Key Pattern | Scope |
|---|---|---|
| Contacts | `contacts<pubKey10>` | Per device identity |
| Messages | `messages_<pubKey10><contactKey>` | Per device + contact |
| Channel Messages | `channel_messages_<pubKey10><index>` | Per device + channel |
| Channels | `channels<pubKey10>` | Per device identity |
| Channel Order | `channel_order_<pubKey10>` | Per device identity |
| Contact Groups | `contact_groups<pubKey10>` | Per device identity |
| Communities | `communities_v1<pubKey10>` | Per device identity |
| Unread Counts | `contact_unread_count<pubKey10>` | Per device identity |
| Discovered Contacts | `discovered_contacts` | Global |
| App Settings | `app_settings` | Global |
| Path History | `path_history_<contactKey>` | Per contact |
## Auto-Add Configuration Bitmask
Used by `CMD_SET_AUTO_ADD_CONFIG` (58) and `RESP_CODE_AUTO_ADD_CONFIG` (25):
| Bit | Flag | Description |
|-----|------|-------------|
| 0 | 0x01 | Overwrite oldest contact when list is full |
| 1 | 0x02 | Auto-add chat users |
| 2 | 0x04 | Auto-add repeaters |
| 3 | 0x08 | Auto-add room servers |
| 4 | 0x10 | Auto-add sensors |
## Radio Packet Payload Types
Seen inside `PUSH_CODE_LOG_RX_DATA` raw packets:
| Code | Type |
|------|------|
| 0x00 | REQ (request) |
| 0x01 | RESPONSE |
| 0x02 | TXTMSG (text message) |
| 0x03 | ACK |
| 0x04 | ADVERT |
| 0x05 | GRPTXT (group/channel text) |
| 0x06 | GRPDATA (group data) |
| 0x07 | ANONREQ (anonymous request) |
| 0x08 | PATH |
| 0x09 | TRACE |
| 0x0A | MULTIPART |
| 0x0B | CONTROL |
| 0x0F | RAW_CUSTOM |
## State Management
Uses Flutter `Provider` with `ChangeNotifier`. The central state holder is `MeshCoreConnector`, which owns all in-memory collections and fires debounced (50ms) `notifyListeners()` to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand.
### Data Flow
1. Raw frames arrive over BLE/USB/TCP
2. First byte is parsed as response/push code
3. Appropriate model factory (`fromFrame()`) parses the data
4. In-memory collections are updated
5. Storage stores are persisted (async)
6. `notifyListeners()` triggers UI rebuilds
7. Screens read current state via getters
+164
View File
@@ -0,0 +1,164 @@
# Channels
## Overview
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
Up to 8 channels (indices 07) can be active simultaneously on one device.
## How to Access
QuickSwitchBar tab 1 (middle) from any main screen.
## Channel Types
| Type | Icon | Color | Description |
|---|---|---|---|
| Public | Globe | Green | Fixed well-known PSK; any device can join |
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret |
## Channels List Screen
### What the User Sees
- **Search bar** with live text filtering (300ms debounce)
- **Sort/filter button**
- **Scrollable list of channel cards**, each showing:
- Type icon with color coding (purple badge overlay for community channels)
- Channel name (or "Channel N" if unnamed)
- Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}"
- Unread badge (if messages are unread)
- Drag handle (when manual sort is active)
- **"+" FAB** to add a new channel
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware.
### Sorting Options
- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active)
- **AZ**: Alphabetical
- **Latest messages**: Most recent first
- **Unread**: Most unread first
## Adding a Channel
Tap the "+" FAB to open a dialog with six options:
1. **Create Private Channel** — Enter a name (max 31 characters); a random PSK is generated
2. **Join Private Channel** — Enter a name and a 32-hex PSK (non-hex characters like spaces and dashes are silently stripped, so pasted keys with formatting are accepted)
3. **Join Public Channel** — One tap; uses the well-known public PSK (only shown if no public channel exists)
4. **Join Hashtag Channel** — Enter a hashtag name; PSK is derived from the name. If communities exist, choose between regular hashtag (SHA-256) or community hashtag (HMAC)
5. **Scan Community QR** — Opens QR scanner to join a community
6. **Create Community** — Enter a name; generates a random 32-byte secret; optionally adds a community public channel; shows QR code for sharing
## Channel Actions (Long-Press / Right-Click)
| Action | Description |
|---|---|
| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) |
| Mute / Unmute | Toggle push notification suppression for this channel |
| Delete | Remove the channel from the device (confirmation required) |
## Channel Chat
Tap a channel card to open the channel chat screen.
### App Bar
- Type icon (public/private/hashtag)
- Channel name
- Subtitle: "{type} - {N} unread"
### Message Display
- Reverse-scrolling list (newest at bottom)
- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble
- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle)
- Automatic older-message loading on scroll-to-top
- Jump-to-bottom button when scrolled up
- **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset text size
- **Message tracing mode** (when enabled in App Settings): Each bubble additionally shows path prefix bytes (`via XX,YY,...`), a timestamp, and a repeat count icon
### Message Types in Chat
- **Plain text** with linkified URLs
- **GIFs** (`g:{gifId}`) rendered inline via Giphy CDN
- **Location pins** (`m:{lat},{lon}|{label}|`) shown as tappable location cards
- **Reactions** displayed as emoji pills below target messages
### Replies (Channel Chat Only)
- **Mobile**: Swipe an **incoming** message left to trigger reply (with haptic feedback). You cannot swipe your own outgoing messages. Swipe reply is not available on desktop.
- **All platforms**: Long-press → "Reply"
- Reply banner appears above the input bar with the quoted message (tap X to cancel)
- Sent replies are prefixed `@[{senderName}] {text}`
- Received replies show a bordered quote block inside the bubble; tapping scrolls to the original. Reply previews render GIF thumbnails and location pin icons, not just text.
### Message Path Viewing
- **Mobile**: Tap a message bubble to view its routing path
- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
### Context Actions (Long-Press / Right-Click)
| Action | Availability | Description |
|---|---|---|
| Reply | All messages | Triggers reply mode |
| Path | Desktop only | Opens message path view |
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
| Copy | All messages | Copies text to clipboard |
| Delete | All messages | Removes locally (not from mesh) |
### Message Path Viewing
Tap a message bubble to open the Channel Message Path Screen, which shows:
- Each hop in the path as a visual chain
- Known contacts identified by name at each hop
- Observed vs. declared hop counts
- Alternative path variants (if received via multiple paths)
- Map view buttons for geographic path visualization
## Communities
Communities are a layer above channels that provide a private namespace.
### What is a Community?
A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret:
- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]`
- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]`
Outsiders who don't know the secret cannot discover or join community channels.
### Sharing a Community
Communities are shared via QR codes containing a JSON payload:
```json
{"v": 1, "type": "meshcore_community", "name": "...", "k": "<base64url-secret>"}
```
### Managing Communities
From the channels screen overflow menu → "Manage Communities". Opens a draggable scrollable sheet (resizable 3090% of screen height):
- Each community shows its name and a short community ID (first 8 hex characters)
- **Tap a community** to directly show its QR code for sharing
- **Popup menu** per community:
- **Show QR** — displays the QR code for sharing with new members
- **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
## How Channels Differ from Direct Messages
| Aspect | Channels | Direct Messages |
|---|---|---|
| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact |
| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) |
| Sender identity | Plain text prefix in payload | Verified via public key |
| Replies | Supported (swipe or long-press) | Not supported |
| Retry mechanism | No automatic retry | Exponential backoff with path rotation |
+120
View File
@@ -0,0 +1,120 @@
# Chat & Messaging
## Overview
The app supports two chat modes:
- **Direct messages**: Encrypted point-to-point messages to individual contacts
- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md))
This page covers direct messaging. For channel chat, see the Channels documentation.
## How to Access
From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
## Chat Screen Layout
### App Bar
- **Title**: Contact name
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
- **Action buttons**:
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
- **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
- **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
### Message List
- Scrollable list with newest messages at the bottom
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
- Bubble width capped at 65% of screen width
- Hyperlinks rendered as tappable green underlined text
- **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset
- **Jump to bottom**: Floating button appears when scrolled away from the bottom
- **Lazy loading**: Scrolling to top loads older messages from storage
### Input Bar
- **GIF button** (left): Opens GIF picker bottom sheet
- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time
- **Send button** (right): Submits the message
- On desktop: Enter/Numpad Enter also submits
- When a GIF is selected, the text field shows an inline GIF preview with a dismiss button
## Message Types
| Type | Wire Format | Display |
|---|---|---|
| Plain text | Raw UTF-8 string | Inline text with link detection |
| GIF | `g:<giphy-id>` | Inline GIF image from Giphy CDN |
| Location pin | `m:<lat>,<lon>\|<label>\|...` | Location icon + label; tap to open map |
| Reaction | `r:<hash>:<emoji-index>` | Applied to target message as emoji pill |
## Message Status
Outgoing messages display a status indicator:
| Status | Icon | Meaning |
|---|---|---|
| Pending | Grey double-check | Queued, waiting for device to transmit (visually identical to Sent) |
| Sent | Grey double-check | Device confirmed transmission (visually identical to Pending) |
| Delivered | Green double-check | Remote node acknowledged receipt |
| Failed | Red X | All retries exhausted |
### Message Tracing Mode
When enabled in App Settings, additional metadata appears inside each bubble:
- Timestamp (HH:MM)
- Retry count (e.g., "Retry 2 of 4")
- Status icon
- Round-trip time in seconds (if delivered)
## Message Length Limits
- **Direct messages**: 156 bytes (UTF-8) — enforced in real-time by the input formatter
- **Channel messages**: 160 minus sender name length minus 2 bytes for the `"<name>: "` prefix
- Over-length paste shows a snackbar error
## Send Queue
Only one message per contact can be in-flight at a time (to avoid overflowing the firmware's 8-entry ACK table). If you send multiple messages rapidly, they are queued and sent sequentially — each waits for the previous one to be delivered, fail, or exhaust retries before transmitting.
## Retry Mechanism
When a direct message is sent:
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise `3000 + 3000 × path_length` milliseconds (15000ms for flood)
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
5. **Max retries**: Configurable (default 5, range 210)
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
7. If **Clear Path on Max Retry** is enabled (App Settings), the contact's stored routing path is automatically cleared when max retries are exhausted
8. **Auto route rotation**: When enabled (and no manual path override is set), the retry service uses a diversity window to avoid re-using recently tried paths, cycling through known routes on each attempt
### Manual Retry
Long-press a failed message → "Retry" to re-send using the current routing settings.
## Reactions
Add emoji reactions to incoming messages (not your own):
1. Long-press (or right-click on desktop) a message
2. Select "Add reaction" from the context menu
3. Choose from quick emojis (thumbs up, heart, laugh, party, clap, fire) or browse the full emoji picker
4. Reactions appear as pills below the message bubble with emoji and count
5. Pending reactions show at 50% opacity with a spinner
6. Failed reactions show a red retry icon (tap to retry)
## Context Actions (Long-Press / Right-Click)
| Action | Availability | Description |
|---|---|---|
| Add reaction | Incoming messages only | Opens emoji picker |
| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
| Copy | All messages | Copies text to clipboard |
| Delete | All messages | Removes locally (not from mesh) |
| Retry | Failed outgoing messages | Re-sends the message |
| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
+118
View File
@@ -0,0 +1,118 @@
# Contacts
## Overview
The Contacts screen is the primary hub for managing mesh nodes your radio has a relationship with. A "contact" is any node whose cryptographic advertisement has been received — it can be a chat user, repeater, room server, or sensor.
## How to Access
- Automatically shown after connecting to a device
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
- Back navigation from Chat or Settings screens
## Contact Types
| Type | Avatar Color | Icon | Description |
|---|---|---|---|
| Chat | Blue | Chat bubble | Another user's mesh radio |
| Repeater | Orange | Cell tower | A mesh repeater/relay node |
| Room | Purple | Group | A room server for group chat |
| Sensor | Green | Sensors | A sensor device |
## Contact List
Each contact is displayed as a list tile showing:
- **Avatar**: Color-coded circle with type icon (or first emoji of the contact's name if it starts with one)
- **Name**: Contact name (single line)
- **Path label**: "Direct", "N hops", or "Flood" (with forced variants if a path override is active)
- **Public key**: Shortened hex format `<XXXXXXXX...XXXXXXXX>`
- **Unread badge**: Red pill with count (if unread messages exist)
- **Last seen**: Relative timestamp ("Now", "5 mins ago", "2 hours ago", "3 days ago"). For chat contacts, this shows whichever is more recent: the last advertisement time or the last message time
- **Favorite star**: Amber star icon if favorited
- **Location pin**: Grey pin icon if the contact has GPS coordinates
Pull-to-refresh re-fetches the full contact list from the device.
## Search and Filter
A toolbar at the top provides:
**Search**: Matches contact name (case-insensitive) or public key hex prefix. Debounced at 300ms.
**Sort options**:
- Latest Messages (by most recent message)
- Heard Recently (by last seen / last message)
- AZ (alphabetical)
**Filter options**:
- All, Favorites, Users, Repeaters, Room Servers, Unread Only
## Contact Groups
Groups are a client-side organizational feature for grouping contacts.
- **Create a group**: Tap the group dropdown → "+" icon → enter name → select members → Save
- **Edit a group**: Group dropdown → pencil icon next to the group
- **Delete a group**: Group dropdown → trash icon next to the group
- **Filter by group**: Select a group from the dropdown to show only its members
Groups are stored per radio identity (scoped by public key).
**Validation rules**: Group names cannot be empty, cannot be "all" (reserved, case-insensitive), and must be unique (case-insensitive). The group creation dialog includes a built-in search field to filter contacts when selecting members. Creating a new group automatically selects it as the active filter.
## Tap Actions
| Contact Type | Action on Tap |
|---|---|
| Chat / Sensor | Opens ChatScreen for direct messaging |
| Repeater | Shows password login dialog → opens RepeaterHubScreen |
| Room | Shows password login dialog → opens ChatScreen for room chat |
## Long-Press / Right-Click Menu
| Action | Availability | Description |
|---|---|---|
| Path Trace / Ping | Repeaters, Rooms (always); Chat if `pathLength > 0` | Opens PathTraceMapScreen. Label shows "Ping" when no path bytes are known, "Path Trace" otherwise |
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
| Room Login | Rooms only | Login dialog → ChatScreen |
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
| Open Chat | Chat/Sensor | Same as single tap |
| Add/Remove Favorite | All types | Toggles the favorite flag |
| Share Contact | All types | Copies `meshcore://<hex>` URI to clipboard |
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
## App Bar Menus
The Contacts screen has **two separate popup menus** in the app bar:
**Antenna icon menu** (contact sharing):
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
- Flood Advert — broadcasts across the full mesh network
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
**Three-dot overflow menu**:
- Disconnect — disconnects from the device
- Discovered Contacts — opens the DiscoveryScreen
- Settings — opens the Settings screen
## Adding Contacts
### Automatic (Passive)
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
### Import from Clipboard
Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
### Import from Discovered Contacts
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
## Contact Sharing Format
Contacts are shared using the `meshcore://` URI scheme:
```
meshcore://<hex-encoded-advertisement-packet>
```
This contains the node's public key and metadata. Paste it into another MeshCore app to import.
+186
View File
@@ -0,0 +1,186 @@
# Map & Location
## Overview
The Map feature is a full-featured node-location visualization and radio-planning tool built on OpenStreetMap tiles. It is one of the three primary views accessible from the QuickSwitchBar.
## How to Access
- **QuickSwitchBar tab 2** (rightmost) from Contacts or Channels
- **Deep-link from a chat message**: Tapping a shared location pin in a chat opens the map centered on that pin
- **Settings → Offline Map Cache**: Opens the tile cache management screen
## What the Map Displays
### Self Location (Teal Circle)
Your own node's position, obtained from the device firmware. Displayed as a teal `person_pin_circle` icon. Only appears if the device has GPS data or a manually-set location.
### Contact / Node Markers (Color-Coded)
All contacts with known GPS coordinates are plotted:
| Type | Color | Icon |
|---|---|---|
| Chat user | Blue | Person |
| Repeater | Green | Router |
| Room | Purple | Meeting room |
| Sensor | Orange | Sensors |
Node name labels appear automatically at zoom level 12 and above.
### Shared Map Pins (Flag Icons)
Location pins shared in chat messages are displayed as flags:
- **Blue flag**: From a direct message
- **Purple flag**: From a private channel
- **Orange flag**: From a public channel
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
### Predicted / Guessed Locations (Semi-Transparent)
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
#### Why guessed locations exist
In a mesh network, every message hops through one or more repeaters on its way to the destination. Each repeater in the path is identified by the first byte of its public key. If any of those repeaters have a known GPS location (because they advertise it), then a contact that routes through those repeaters must be somewhere within radio range of them. By combining the positions of multiple repeaters a contact is known to use, the app can triangulate a rough area where the contact is likely located.
#### How the algorithm works
1. **Build a repeater index**: The app collects all known contacts of type Repeater that have a valid GPS position and indexes them by the first byte of their public key.
2. **Collect anchor points**: For each contact that lacks GPS, the app looks at the **last-hop byte** of the contact's current path and also searches the `PathHistoryService` for recent paths. Each last-hop byte that matches a located repeater becomes an "anchor point" — a GPS coordinate the contact is likely near.
3. **Resolve ambiguity**: If multiple repeaters share the same first public-key byte (a hash collision), that byte is discarded as ambiguous. Only unambiguous one-to-one matches are kept.
4. **Filter geometric inconsistencies**: Two anchor points separated by more than `2 × maxRangeKm` (the estimated LoRa radio range, computed from the current frequency, bandwidth, spreading factor, and TX power using a free-space path loss model) cannot both be in range of the same node. Outlier anchors are removed to keep only a geometrically consistent set.
5. **Compute the estimated position**:
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
- **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80120m) applied for visual separation.
6. **Assign confidence level**:
- **High confidence** (2+ anchors): Displayed at 55% opacity.
- **Low confidence** (1 anchor): Displayed at 30% opacity.
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
#### How to read guessed locations on the map
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
- **More transparent** (30%): Lower confidence — based on a single repeater anchor only.
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
## Map Interactions
### Zoom and Pan
Standard pinch-to-zoom (range 218). Initial camera position is calculated from the statistical spread of all plotted points.
### Tap on a Node Marker
Opens a dialog showing: type, path (hop chain), coordinates, last-seen time, and public key. Action buttons vary by type:
- **Chat nodes**: "Open Chat"
- **Repeaters**: "Manage Repeater"
- **Rooms**: "Join Room"
### Long-Press on Empty Map Area
Shows a bottom sheet with:
- **Share marker here**: Prompts for a label, then pick a DM contact or channel to send the location to. Wire format: `m:<lat>,<lon>|<label>|poi`
- **Set as my location**: Updates your device's advertised location
### Filter Dialog (FAB)
Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts.
Additional filters:
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
- **Last-seen time slider**: From 1 hour to "all time"
### Legend Card (Top-Right)
Shows node count and pin count. Tappable to expand a legend of all marker types.
---
## Path Trace Map
### How to Access
- From the main map's radar icon
- From a contact's long-press menu → "Path Trace / Ping"
- From a message's path view → radar icon
### What the User Sees
A map with a polyline showing the route from your node through repeater hops to the target:
- **Green circles**: Hops with known GPS coordinates
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
- **Red endpoint**: Target contact with known GPS
- **Purple semi-transparent endpoint**: Target with guessed position
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
### How It Works
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
---
## Line-of-Sight (LOS) Analysis
### How to Access
From the main map, tap the terrain/antenna icon.
### What the User Sees
A full-screen map with a collapsible control panel containing:
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0400 ft), Run LOS button
### Key Interactions
- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable)
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
- **Antenna heights** are adjustable for both endpoints
- **Map line** between endpoints is colored green (clear) or red (blocked)
- Terrain elevation is fetched from the Open-Meteo API (2181 sample points, cached 24 hours)
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
---
## Offline Map Cache
### How to Access
Settings → App Settings → Map Display → Offline Map Cache
### What the User Sees
- Map with a blue polygon overlay showing previously selected cache bounds
- Bounding box coordinates card
- **Cache Area** controls: "Use Current View" and Clear buttons
- **Zoom Range** slider (318) with estimated tile count
- **Download progress** bar (when downloading)
- **Download Tiles** and **Clear Cache** buttons
### Key Interactions
1. Pan/zoom the map to the desired area
2. Tap "Use Current View" to capture the viewport as cache bounds
3. Adjust the zoom range slider
4. Tap "Download Tiles" (confirmation dialog shows estimated count)
5. Tiles are downloaded with up to 8 concurrent connections
6. Once cached, tiles are served from disk without internet (365-day stale period)
---
## GPX Export
### How to Access
Settings → Export section
### What It Does
Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Three export options:
- **Export Repeaters**: Repeater and Room contacts with locations
- **Export Contacts**: Chat contacts with locations
- **Export All**: All contacts with locations
Each waypoint includes: name, lat/lon, type label, and public key hex.
---
## Location Data Sources
The phone's own GPS is **never used**. All location data comes from the mesh:
1. **Device self-location**: Read from firmware device-info response. Set manually in Settings → Location, or updated automatically if the device has a GPS module.
2. **Remote node locations**: Extracted from advertisement packets received over the mesh. Encoded as integer lat/lon × 1,000,000.
+87
View File
@@ -0,0 +1,87 @@
# Navigation
## App Flow
The app follows this general flow:
```
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen
```
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
## Quick Switch Bar
The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual treatment (blur backdrop, transparent theme, rounded corners). It appears at the bottom of all three main screens.
| Index | Icon | Label | Screen |
|---|---|---|---|
| 0 | People | Contacts | ContactsScreen |
| 1 | Tag | Channels | ChannelsScreen |
| 2 | Map | Map | MapScreen |
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
## Device Screen
The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
### What the User Sees
**App Bar**:
- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
- Right: Disconnect button (`bluetooth_disabled` crossed-out icon) and Settings button (tune icon)
**Body**:
- **Connection Card**: Device avatar, device name, device ID, "Connected" chip, and battery chip
- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
### Disconnection
- The disconnect button shows a confirmation dialog before disconnecting
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
## Theme and Locale
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
- **Language** can be overridden to one of 15 supported languages, or follow the system locale
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
## Full Navigation Graph
```
ScannerScreen (root, always on stack)
├─ [BLE connect] → push → ContactsScreen
├─ [TCP FAB] → push → TcpScreen
│ └─ [TCP connected] → pushReplacement → ContactsScreen
└─ [USB FAB] → push → UsbScreen
└─ [USB connected] → pushReplacement → ContactsScreen
ContactsScreen (selected=0)
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
├─ [quick-switch 2] → pushReplacement → MapScreen
├─ [tap contact] → push → ChatScreen
├─ [overflow > Settings] → push → SettingsScreen
└─ [overflow > Discovered] → push → DiscoveryScreen
ChannelsScreen (selected=1)
├─ [quick-switch 0] → pushReplacement → ContactsScreen
├─ [quick-switch 2] → pushReplacement → MapScreen
├─ [tap channel] → push → ChannelChatScreen
└─ [overflow > Settings] → push → SettingsScreen
MapScreen (selected=2)
├─ [quick-switch 0] → pushReplacement → ContactsScreen
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
├─ [radar button] → push → PathTraceMapScreen
├─ [terrain button] → push → LineOfSightMapScreen
└─ [long-press] → share marker / set location
Settings (push from any main screen)
└─ [App Settings] → push → AppSettingsScreen
└─ [Offline Map Cache] → push → MapCacheScreen
```
Any disconnection from any screen triggers `popUntil(route.isFirst)`, returning to the Scanner.
+92
View File
@@ -0,0 +1,92 @@
# Notifications
## Overview
MeshCore Open provides both **system notifications** (push-style OS alerts) and **in-app unread badges** to inform users of new activity.
## Notification Types
### 1. Direct Message Notifications
- **Triggered when**: A new incoming message arrives from a Chat or Room contact
- **Title**: Contact's name
- **Body**: Message text (reactions show "Reacted [emoji]", GIFs show "Sent a GIF")
- **Priority**: High
- **Android channel**: `messages`
### 2. Channel Message Notifications
- **Triggered when**: A new message arrives on a non-muted channel
- **Title**: Channel name (or "Channel N" if unnamed)
- **Body**: `"<senderName>: <message text>"`
- **Priority**: High
- **Android channel**: `channel_messages`
### 3. Advertisement Notifications
- **Triggered when**: A new node is discovered on the mesh for the first time
- **Title**: "New [type] discovered" (e.g., "New chat node discovered")
- **Body**: Contact's name
- **Priority**: Default
- **Android channel**: `adverts`
### 4. Background Service Notification (Android Only)
- A persistent low-priority notification: "MeshCore running — Keeping BLE connected"
- Required by Android for foreground services to keep BLE alive in the background
- Tap to re-launch the app
- **Does not auto-start on reboot** — the user must re-open the app manually after a phone restart
### Notification Tap Behavior
Tapping a notification currently re-launches the app at the root route. It does **not** navigate directly to the relevant chat or channel.
## In-App Unread Badges
Red numeric badges appear throughout the UI:
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
- **Channels list**: Each channel row shows an unread badge
- **Chat screen subtitle**: Shows unread count inline
- Badges cap at "99+" for display
### How Unread Counts Work
- Stored per contact (by public key) and per channel, **scoped to the connected device's identity** (first 10 hex characters of its public key). Switching between different radios gives each its own independent unread state
- **Suppressed when viewing**: Opening a chat resets the count to 0 and cancels the OS notification
- **Ignored for**: Outgoing messages, CLI messages, and repeater contacts
- Debounced writes (500ms) to avoid excessive storage I/O during message bursts
## Notification Settings
Access via **App Settings → Notifications**:
| Setting | Default | Description |
|---|---|---|
| Enable Notifications | On | Master toggle; requests OS permission when turned on |
| Message Notifications | On | DM alerts (greyed out if master is off) |
| Channel Message Notifications | On | Channel alerts (greyed out if master is off) |
| Advertisement Notifications | On | New node alerts (greyed out if master is off) |
### Per-Channel Muting
Long-press a channel in the channels list → "Mute channel" / "Unmute channel". Muted channels do not generate OS notifications.
There is no per-contact muting.
## Rate Limiting
The notification system prevents notification storms:
- **Minimum interval**: 3 seconds between individual notifications
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
## Notification Clearing
- **Opening a contact chat**: Cancels the OS notification and resets unread count
- **Opening a channel**: Cancels the channel notification and resets unread count
- **Opening Contacts screen**: Cancels all advertisement notifications
## Platform Support
| Platform | Message Notifs | Badge | Background Service |
|---|---|---|---|
| Android | Yes | Via notification number | Yes (foreground service) |
| iOS | Yes | Yes (app badge) | No |
| macOS | Yes | Yes | No |
| Windows | Yes | No | No |
| Linux | Yes (if D-Bus available) | No | No |
+186
View File
@@ -0,0 +1,186 @@
# Repeater Management
## Overview
Repeater Management provides tools for administering MeshCore repeater and room server nodes. It includes device status monitoring, CLI access, telemetry reading, neighbor discovery, and remote configuration.
## How to Access
From the Contacts screen:
1. Long-press a **Repeater** or **Room** contact
2. Select "Manage Repeater" or "Room Management"
3. Enter the admin password in the login dialog
4. Navigate to the Repeater Hub Screen
### Login Dialog
- Password field with show/hide toggle
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password stops immediately after the first attempt — only timeouts trigger retries
- After 5 failed attempts, further login attempts are blocked
---
## Repeater Hub Screen
The central management screen showing:
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
- **Battery chemistry selector**: NMC / LiFePO4 / LiPo (saved per repeater)
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" based on contact type:
| Card | Destination |
|---|---|
| Status | Repeater Status Screen |
| Telemetry | Telemetry Screen |
| CLI | Repeater CLI Screen |
| Neighbors | Neighbors Screen |
| Settings | Repeater Settings Screen |
---
## Repeater Status
### What the User Sees
Three information cards:
**System Information**:
- Battery percentage
- Uptime
- Queue length
- Error flags
- Clock at login time
**Radio Statistics**:
- Last RSSI and SNR
- Noise floor
- TX and RX airtime
**Packet Statistics**:
- Packets sent, received, and duplicates
- Broken down by flood vs. direct
### Key Interactions
- Auto-queries the repeater on open; shows a loading spinner until data arrives
- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
- Pull-to-refresh or refresh button to re-query
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
---
## Repeater CLI
A terminal-style interface for sending commands directly to the repeater.
### What the User Sees
- **Quick-command bar** (horizontal scroll): Shortcut buttons for common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
- **Command history list**: Sent commands in primary color, responses in secondary color
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
### Key Interactions
- Type a command and press send (or Enter on desktop)
- Up/down arrows navigate through command history
- Quick-command buttons populate and send common commands
- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
- Clear icon: Wipes the command/response history
- Failed/timed-out commands are automatically retried once
### Available CLI Commands
**General**: `advert`, `reboot`, `clock`, `password`, `ver`, `clear stats`
**Settings**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
**Bridge**: `get bridge.type`
**Logging**: `log start`, `log stop`, `log erase`
**Neighbors**: `neighbors`, `neighbor.remove`
**Region Management**: `region`, `region load/get/put/remove/allowf/denyf/home/save`
**GPS**: `gps`, `gps on/off/sync/setloc/advert`
---
## Telemetry
### What the User Sees
A list of Cayenne LPP sensor channel cards:
- **Channel 1** (special): Battery voltage (shown as percentage or raw mV) and MCU temperature
- **Other channels**: Raw sensor values with appropriate labels
Shows "No data" until a response arrives from the repeater.
### Key Interactions
- Auto-queries on open
- Pull-to-refresh
- Temperature respects metric/imperial setting
- Battery readings are stored for the repeater's battery snapshot
---
## Neighbors
### What the User Sees
A card titled "Repeater's Neighbors - N" listing each neighbor as:
- Repeater name (or hex key prefix if unknown)
- Time since last heard
- SNR quality icon with color coding and label
### Key Interactions
- Auto-queries up to 15 neighbors on open
- Matches public key prefixes against known contacts to show names
- Pull-to-refresh
---
## Repeater Settings
### What the User Sees
Five configuration cards:
**1. Basic Settings**
- Name field
- Admin password field
- Guest password field
**2. Radio Settings**
- Frequency (MHz)
- TX Power (dBm)
- Bandwidth dropdown (kHz)
- Spreading Factor (SF5SF12)
- Coding Rate (4/54/8)
**3. Location Settings**
- Latitude and longitude fields
**4. Features**
- Packet forwarding toggle
- Guest access toggle
**5. Advertisement Settings**
- Local advert interval slider (60240 minutes) with enable/disable toggle
- Flood advert interval slider (3168 hours) with enable/disable toggle
**6. Danger Zone** (red-styled card)
- Reboot repeater
- Erase filesystem (serial-only warning)
### Key Interactions
- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
- Save button appears when changes are detected
- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
- Validation prevents invalid values (e.g., frequency range, LoRa parameter compatibility)
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly
+124
View File
@@ -0,0 +1,124 @@
# Scanner & Connection
## BLE Scanner (Home Screen)
The BLE Scanner is the app's home screen, displayed immediately on launch.
### How to Access
- Opens automatically when the app starts
- Returns here when disconnecting from any device
- Accessible by navigating back from a connected session
### What the User Sees
**App Bar**: Centered title "Scanner".
**Bluetooth-Off Warning Banner** (conditional): Appears when the Bluetooth adapter is off, showing a `bluetooth_disabled` icon, a warning message, and on Android, an "Enable Bluetooth" button.
**Status Bar**: A full-width colored strip reflecting the current connection state:
| State | Text | Color |
|---|---|---|
| Disconnected | "Not connected" | Grey |
| Scanning | "Scanning..." | Blue |
| Connecting | "Connecting..." | Orange |
| Connected | "Connected to \<device name\>" | Green |
| Disconnecting | "Disconnecting..." | Orange |
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
**Bottom FAB Row**: Up to three floating action buttons:
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
### Device Tile
Each discovered device is displayed as a list tile showing:
- **Signal strength icon** (color-coded by RSSI):
- Green: >= -60 dBm (excellent)
- Light green: -60 to -70 dBm (good)
- Amber: -70 to -80 dBm (fair)
- Orange: -80 to -90 dBm (weak)
- Red: < -90 dBm (poor)
- **RSSI value** in dBm (e.g., "-72 dBm")
- **Device name** (falls back to "Unknown Device")
- **Device ID** (BLE MAC address on Android; a system-assigned UUID on iOS/macOS)
- **Connect button** (the entire tile row is also tappable — both trigger connection)
Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon shape and are only differentiated by color (orange vs. red).
### How Scanning Works
- Filters for devices with names starting with `MeshCore-` or `Whisper-`
- Uses low-latency scan mode on Android
- Scans for 10 seconds then auto-stops
- On iOS/macOS, waits for BLE adapter initialization before starting
- If Bluetooth is turned off during a scan, scanning stops immediately
### Connecting to a Device
Tap a device tile or its Connect button:
1. The connector stops scanning and transitions to "connecting"
2. Connects to the device with a 15-second timeout
3. Requests MTU 185 bytes for optimal throughput
4. Discovers BLE services and locates the Nordic UART Service
5. Subscribes to TX notifications for receiving data
6. On success, automatically navigates to the Contacts screen
7. On failure, shows a red error snackbar
---
## USB Connection
### How to Access
From the Scanner screen, tap the **USB** FAB button.
### What the User Sees
- A colored status bar at the top (same color scheme as BLE scanner)
- A list of detected USB serial ports, each showing:
- Friendly display name
- Raw port name (subtitle, only shown when it differs from the display name)
- "Connect" button
- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
### Key Interactions
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
- On mobile: tap the "Scan" FAB to manually refresh
- Tap a port or its Connect button to connect
- On successful connection, navigates to Contacts screen
- On connection failure, the port list automatically refreshes
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
---
## TCP Connection
### How to Access
From the Scanner screen, tap the **TCP/IP** FAB button.
### What the User Sees
- A colored status bar at the top
- **Host address** text field
- **Port number** text field
- **Connect** button
- FABs at the bottom to switch to USB or BLE
### Key Interactions
- Last-used host and port are pre-populated from saved settings
- Tap Connect to validate inputs and connect
- Host must not be empty
- Port must be a number between 1 and 65535
- Validation errors are shown as red snackbars
- The Connect button shows a spinner and "Connecting..." label while in progress
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
- On success, navigates to Contacts screen and saves the host/port to settings
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
- Error messages for timeout, unsupported platform, and connection failures
+169
View File
@@ -0,0 +1,169 @@
# Settings
## How to Access
- From the Device Screen: tap the tune/sliders icon in the app bar
- From Contacts or Channels: overflow menu (three-dot) → Settings
Settings are only accessible while a device is connected.
## Settings Screen Layout
The settings screen is a scrollable list of cards:
1. [Device Info](#device-info)
2. [App Settings](#app-settings) (link to sub-screen)
3. [Node Settings](#node-settings)
4. [Actions](#actions)
5. [Debug](#debug)
6. [Export](#export)
7. [About](#about)
---
## Device Info
A collapsible card showing read-only device information. **Collapsed by default** — tap the header to expand with an animated chevron indicator:
| Field | Description |
|---|---|
| Name | Connected device's display name |
| ID | Device identifier |
| Status | Connected / Disconnected |
| Battery | Percentage or voltage (tap to toggle) |
| Node Name | The node's mesh identity name |
| Public Key | First 16 hex characters + "..." |
| Contacts Count | Number of known contacts |
| Channel Count | Number of configured channels |
Battery shows an alert icon and orange text when at 15% or below. The toggle only works when millivolt data is available from the firmware.
---
## App Settings
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
### Appearance
- **Theme**: System / Light / Dark
- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
### Notifications
- **Master enable/disable**: Requests OS permission when enabling
- **Message notifications**: New direct message alerts
- **Channel message notifications**: New channel message alerts
- **Advertisement notifications**: New node discovery alerts
### Messaging
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
- Max Route Weight (110, default 5, integer steps)
- Initial Route Weight (0.55.0, default 3.0)
- Success Increment (0.12.0, default 0.5, 0.1 steps)
- Failure Decrement (0.12.0, default 0.2, 0.1 steps)
- Max Message Retries (210, default 5)
### Battery
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
### Map Display
- **Show Repeaters**: Toggle repeater markers on map
- **Show Chat Nodes**: Toggle chat node markers
- **Show Other Nodes**: Toggle room/sensor markers
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
- **Units**: Metric / Imperial
- **Offline Map Cache**: Navigate to tile download screen
### Debug
- **App Debug Logging**: Enable the in-app debug log
---
## Node Settings
These settings are sent directly to the connected device firmware.
### Node Name
- Opens a dialog with a text field (max 31 characters)
- Sends the new name to the device
- Confirmed via snackbar
### Radio Settings
Opens a dialog pre-populated with the device's current radio settings. Contains:
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
- **Frequency** (MHz): Free text, validated 3002500 MHz
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
- **Spreading Factor**: SF5SF12
- **Coding Rate**: 4/5, 4/6, 4/7, 4/8
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
### Location
Opens a dialog pre-populated with the device's current coordinates (if known):
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
- If GPS-capable hardware (detected via `gps` custom variable):
- GPS Update Interval (seconds, 6086399, default 900 = 15 minutes). Validated and sent separately before lat/lon
- Enable GPS toggle (takes effect immediately, not deferred to Save)
- Validation: lat ±90, lon ±180
### Contact Settings
Five toggles controlling which node types are auto-added when heard:
- Auto-add Chat Users
- Auto-add Repeaters
- Auto-add Room Servers
- Auto-add Sensors
- Overwrite Oldest (when contact list is full)
### Privacy Mode
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
---
## Actions
One-tap device operations:
| Action | Description |
|---|---|
| Send Advertisement | Floods the mesh with your node's advertisement |
| Sync Time | Sends current Unix timestamp to the device |
| Refresh Contacts | Re-requests the full contact list |
| Reboot Device | Confirmation dialog → reboots the device (shown in orange) |
---
## Debug
Two log viewers accessible via list tiles:
### BLE Debug Log
Two views (togglable via segmented button):
- **Frames view**: Direction icon, description, hex preview, timestamp per frame. Long-press to copy hex.
- **Raw Log RX view**: Decoded LoRa packets with route type, payload type, path, and summary.
- Copy-all and Clear buttons in the app bar.
### App Debug Log
Structured log entries (Info / Warning / Error), with tag, message, and timestamp.
- Must be enabled first in App Settings → Debug
- Copy-all and Clear buttons
---
## Export
Three GPX export options (not available on web):
| Option | Exports |
|---|---|
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
| Export Contacts | Chat contacts with GPS coordinates |
| Export All | All contacts with GPS coordinates |
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
---
## About
Shows the standard Flutter about dialog with app name, version, and legal notice.
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
import 'dart:async';
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/tcp_transport_service.dart';
/// Manages TCP transport for MeshCore devices.
///
/// Owns the [TcpTransportService] and TCP-specific connection state.
/// The main [MeshCoreConnector] delegates all TCP operations here.
class MeshCoreTcpConnector {
final TcpTransportService _service = TcpTransportService();
AppDebugLogService? _debugLog;
StreamSubscription<Uint8List>? _frameSubscription;
// --- Getters ---
String? get activeEndpoint => _service.activeEndpoint;
bool get isConnected => _service.isConnected;
// --- Configuration ---
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({required String host, required int port}) async {
_debugLog?.info('TcpConnector.connect endpoint=$host:$port', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.connect(host: host, port: port);
_debugLog?.info(
'TcpConnector.connect done, endpoint=${_service.activeEndpoint}',
tag: 'TCP',
);
}
StreamSubscription<Uint8List> listenFrames({
required void Function(Uint8List) onFrame,
required void Function(Object, StackTrace?) onError,
required void Function() onDone,
}) {
_frameSubscription = _service.frameStream.listen(
onFrame,
onError: onError,
onDone: onDone,
);
return _frameSubscription!;
}
Future<void> cancelFrameSubscription() async {
await _frameSubscription?.cancel();
_frameSubscription = null;
}
Future<void> disconnect() async {
if (!_service.isConnected && _frameSubscription == null) return;
_debugLog?.info('TcpConnector.disconnect', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.disconnect();
}
Future<void> write(Uint8List data) => _service.write(data);
void dispose() {
_frameSubscription?.cancel();
_service.dispose();
}
}
+78
View File
@@ -0,0 +1,78 @@
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/usb_serial_service.dart';
/// Manages USB serial transport for MeshCore devices.
///
/// Owns the [UsbSerialService] and USB-specific connection state.
/// The main [MeshCoreConnector] delegates all USB operations here.
class MeshCoreUsbManager {
MeshCoreUsbManager();
final UsbSerialService _service = UsbSerialService();
AppDebugLogService? _debugLog;
String? _activePortKey;
String? _activePortLabel;
// --- Getters ---
String? get activePortKey => _activePortKey;
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
bool get isConnected => _service.isConnected;
Stream<Uint8List> get frameStream => _service.frameStream;
// --- Configuration ---
Future<List<String>> listPorts() => _service.listPorts();
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
void setFallbackDeviceName(String label) =>
_service.setFallbackDeviceName(label);
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
_debugLog?.info(
'UsbManager.connect: portName=$portName baud=$baudRate',
tag: 'USB',
);
await _service.connect(portName: portName, baudRate: baudRate);
_activePortKey = _service.activePortKey ?? portName;
_activePortLabel = _service.activePortDisplayLabel ?? portName;
_debugLog?.info(
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
tag: 'USB',
);
}
Future<void> disconnect() async {
if (!_service.isConnected && _activePortKey == null) {
return;
}
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
await _service.disconnect();
_activePortKey = null;
_activePortLabel = null;
}
Future<void> write(Uint8List data) => _service.write(data);
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
// --- Label management ---
void updateConnectedLabel(String selfName) {
_service.updateConnectedLabel(selfName);
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
}
void dispose() {
_service.dispose();
}
}
+214 -102
View File
@@ -1,9 +1,12 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
// Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader {
int _pointer = 0;
int _lastPointer = 0;
final Uint8List _buffer;
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
@@ -13,6 +16,7 @@ class BufferReader {
int readByte() => readBytes(1)[0];
Uint8List readBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
@@ -24,6 +28,7 @@ class BufferReader {
}
void skipBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
@@ -34,10 +39,8 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readCString(int maxLength) {
String readCStringGreedy(int maxLength) {
_lastPointer = _pointer;
final value = <int>[];
final bytes = readBytes(maxLength);
for (final byte in bytes) {
@@ -51,6 +54,25 @@ class BufferReader {
}
}
String readCString({int maxLength = -1}) {
final backupPointer = _pointer;
final value = <int>[];
int counter = 0;
final maxLen = maxLength >= 0 ? maxLength : remaining;
while (counter < maxLen) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
counter++;
}
_lastPointer = backupPointer;
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() =>
@@ -72,6 +94,9 @@ class BufferReader {
if ((value & 0x800000) != 0) value -= 0x1000000;
return value;
}
void resetPointer() => _pointer = 0;
void rewind() => _pointer = _lastPointer;
}
// Buffer Writer - accumulating binary data builder
@@ -114,23 +139,38 @@ class BufferWriter {
}
void writeHex(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException(
'Invalid hex characters at position $i: $hexByte',
);
}
result.add(byte);
}
writeBytes(Uint8List.fromList(result));
writeBytes(hex2Uint8List(hex));
}
void writeBytesPadded(Uint8List bytes, int totalLength) {
// Path data (64 bytes, zero-padded)
final bytesPadded = Uint8List(totalLength);
final len = bytes.length < totalLength ? bytes.length : totalLength;
if (bytes.isNotEmpty && len > 0) {
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
for (int i = 0; i < copyLen; i++) {
bytesPadded[i] = bytes[i];
}
}
writeBytes(bytesPadded);
}
}
Uint8List hex2Uint8List(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException('Invalid hex characters at position $i: $hexByte');
}
result.add(byte);
}
return Uint8List.fromList(result);
}
// Command codes (to device)
@@ -162,15 +202,20 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdGetStats = 56;
const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
const int cmdSetPathHashMode = 61;
// Text message types
const int txtTypePlain = 0;
const int txtTypeCliData = 1;
const int txtTypeSigned = 2;
// Repeater request types (for server requests)
const int reqTypeGetStatus = 0x01;
@@ -200,8 +245,13 @@ const int respCodeDeviceInfo = 13;
const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
const int respCodeAutoAddConfig = 25;
const int respCodeStats = 24;
const int statsTypeCore = 0;
const int statsTypeRadio = 1;
const int statsTypePackets = 2;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -223,6 +273,10 @@ const int advTypeRepeater = 2;
const int advTypeRoom = 3;
const int advTypeSensor = 4;
const int teleModeDeny = 0;
const int teleModeAllowFlags = 1; // use contact.flags
const int teleModeAllowAll = 2;
// Payload Types
const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
@@ -247,8 +301,21 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
const int payloadTypeRawCustom =
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
//auto-add flags
const int autoAddOverwriteOldestFlag =
1 << 0; // 0x01 - overwrite oldest non-favourite when full
const int autoAddChatFlag =
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
const int autoAddRepeaterFlag =
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
const int autoAddRoomServerFlag =
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
const int autoAddSensorFlag =
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
// Sizes
const int pubKeySize = 32;
const int signatureSize = 64;
const int maxPathSize = 64;
const int pathHashSize = 1;
const int maxNameSize = 32;
@@ -291,13 +358,16 @@ const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33;
const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01;
const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
const int contactFlagTeleLoc = 0x04;
const int contactFlagTeleEnv = 0x08; //access environment sensors
const int contactPathLenOffset = 35;
const int contactPathOffset = 36;
const int contactNameOffset = 100;
const int contactTimestampOffset = 132;
const int contactLatOffset = 136;
const int contactLonOffset = 140;
const int contactLastmodOffset = 144;
const int contactLastModOffset = 144;
const int contactFrameSize = 148;
// Message frame offsets
@@ -309,52 +379,44 @@ const int msgTextOffset = 38;
class ParsedContactText {
final Uint8List senderPrefix;
final String text;
const ParsedContactText({required this.senderPrefix, required this.text});
}
ParsedContactText? parseContactMessageText(Uint8List frame) {
if (frame.isEmpty) return null;
final code = frame[0];
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
final message = BufferReader(frame);
try {
final code = message.readByte();
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
return null;
}
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
if (code == respCodeContactMsgRecvV3) {
// Skip SNR and reserved bytes in v3 layout
message.skipBytes(3);
}
final senderPrefix = message.readBytes(6); // public key
message.skipBytes(1); // path length
final textType = message.readByte();
message.skipBytes(4); // timestamp (4 bytes)
final shiftedType = textType >> 2;
final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
if (isSigned) {
// Signed messages have a 4-byte signature after the timestamp, before the text
message.skipBytes(4);
}
final text = message.readCString();
if (text.isEmpty) return null;
return ParsedContactText(senderPrefix: senderPrefix, text: text);
} catch (e) {
debugPrint('Error parsing contact message text: $e');
return null;
}
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
final isV3 = code == respCodeContactMsgRecvV3;
final prefixOffset = isV3 ? 4 : 1;
const prefixLen = 6;
final txtTypeOffset = prefixOffset + prefixLen + 1;
final timestampOffset = txtTypeOffset + 1;
final baseTextOffset = timestampOffset + 4;
if (frame.length <= baseTextOffset) return null;
final flags = frame[txtTypeOffset];
final shiftedType = flags >> 2;
final rawType = flags;
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
if (!isPlain && !isCli) {
return null;
}
var text = readCString(
frame,
baseTextOffset,
frame.length - baseTextOffset,
).trim();
if (text.isEmpty && frame.length > baseTextOffset + 4) {
text = readCString(
frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
).trim();
}
if (text.isEmpty) return null;
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
return ParsedContactText(senderPrefix: senderPrefix, text: text);
}
// Helper to read uint32 little-endian
@@ -377,18 +439,9 @@ int readInt32LE(Uint8List data, int offset) {
return val;
}
// Helper to read null-terminated UTF-8 string
String readCString(Uint8List data, int offset, int maxLen) {
int end = offset;
while (end < offset + maxLen && end < data.length && data[end] != 0) {
end++;
}
try {
return utf8.decode(data.sublist(offset, end), allowMalformed: true);
} catch (e) {
// Fallback to Latin-1 if UTF-8 decoding fails
return String.fromCharCodes(data.sublist(offset, end));
}
// Helper to convert uint32 to hex string
String ackHashToHex(int ackHash) {
return ackHash.toRadixString(16).padLeft(8, '0');
}
// Helper to convert public key to hex string
@@ -448,7 +501,7 @@ Uint8List buildSendTextMsgFrame(
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain);
writer.writeByte(attempt.clamp(0, 3));
writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
writer.writeBytes(recipientPubKey.sublist(0, 6));
writer.writeString(text);
@@ -508,6 +561,17 @@ Uint8List buildGetBattAndStorageFrame() {
return Uint8List.fromList([cmdGetBattAndStorage]);
}
/// Companion radio stats: [56][statsType] where statsType is statsTypeCore/Radio/Packets.
Uint8List buildGetStatsFrame(int statsType) {
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
}
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
Uint8List buildSetPathHashModeFrame(int mode) {
final m = mode.clamp(0, 2);
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
}
// Build CMD_SET_DEVICE_TIME frame
Uint8List buildSetDeviceTimeFrame(int timestamp) {
final writer = BufferWriter();
@@ -628,14 +692,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) {
}
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4]
Uint8List buildUpdateContactPathFrame(
Uint8List pubKey,
Uint8List customPath,
Uint8List path,
int pathLen, {
int type = 1, // ADV_TYPE_CHAT
int flags = 0,
String name = '',
double? lat,
double? lon,
DateTime? lastModified,
}) {
final writer = BufferWriter();
writer.writeByte(cmdAddUpdateContact);
@@ -644,17 +711,7 @@ Uint8List buildUpdateContactPathFrame(
writer.writeByte(flags);
writer.writeByte(pathLen);
// Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i];
}
}
writer.writeBytes(pathPadded);
writer.writeBytesPadded(path, maxPathSize);
// Name (32 bytes, null-padded)
writer.writeCString(name, maxNameSize);
@@ -663,6 +720,27 @@ Uint8List buildUpdateContactPathFrame(
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(timestamp);
if ((lat == null || lon == null) && lastModified != null) {
// If lat/lon not provided, write zeros
writer.writeInt32LE(0);
writer.writeInt32LE(0);
} else {
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
// Latitude
final latitude = lat ?? 0.0;
writer.writeInt32LE((latitude * 1e6).round());
// Longitude
final longitude = lon ?? 0.0;
writer.writeInt32LE((longitude * 1e6).round());
}
if (lastModified != null) {
// Last modified
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(lastModifiedTimestamp);
}
return writer.toBytes();
}
@@ -675,16 +753,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
return writer.toBytes();
}
// Build CMD_GET_RADIO_SETTINGS frame
Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]);
}
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
Uint8List buildGetAutoAddFlagsFrame() {
return Uint8List.fromList([cmdGetAutoAddConfig]);
}
// Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds
@@ -764,7 +841,7 @@ Uint8List buildSendCliCommandFrame(
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData);
writer.writeByte(attempt.clamp(0, 3));
writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
writer.writeBytes(repeaterPubKey.sublist(0, 6));
writer.writeString(command);
@@ -809,10 +886,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(String contactFrame) {
Uint8List buildImportContactFrame(Uint8List contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeHex(contactFrame);
writer.writeBytes(contactFrame);
return writer.toBytes();
}
@@ -826,20 +903,55 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
Uint8List buildSetOtherParamsFrame(
bool allowAutoAddContacts,
int allowTelemetryFlags,
int advertLocationPolicy,
int multiAcks,
) {
final writer = BufferWriter();
writer.writeByte(cmdSetOtherParams);
writer.writeByte(
allowAutoAddContacts ? 0x00 : 0x01,
); // Allow Auto Add Contacts
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
writer.writeByte(0x01);
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
writer.writeByte(multiAcks); // Multi Acknowledgements
return writer.toBytes();
}
// Build CMD_SET_AUTO_ADD_CONFIG frame
// Format: [cmd][flags]
Uint8List buildSetAutoAddConfigFrame({
required bool autoAddChat,
required bool autoAddRepeater,
required bool autoAddRoomServer,
required bool autoAddSensor,
required bool overwriteOldest,
}) {
final writer = BufferWriter();
writer.writeByte(cmdSetAutoAddConfig);
int flags = 0;
if (autoAddChat) flags |= autoAddChatFlag;
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
if (autoAddSensor) flags |= autoAddSensorFlag;
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
writer.writeByte(flags);
return writer.toBytes();
}
//Build CMD_SEND_TELEMETRY_REQ
// Format: [cmd][reserved x3][pub_key? x32]
Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdSendTelemetryReq);
if (pubKey != null && pubKey.length == pubKeySize) {
writer.writeBytes(Uint8List(3)); // reserved bytes
writer.writeBytes(pubKey);
} else {
writer.writeBytes(Uint8List(4)); // reserved bytes
}
return writer.toBytes();
}
+12
View File
@@ -0,0 +1,12 @@
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-",
"HT-",
];
}
+42
View File
@@ -1,8 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
class LinkHandler {
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
final brightness = Theme.of(context).brightness;
final orange = brightness == Brightness.dark
? const Color(0xFFFFB74D)
: const Color(0xFFE65100);
return base.copyWith(color: orange, decoration: TextDecoration.underline);
}
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
static Widget buildLinkifyText({
required BuildContext context,
required String text,
required TextStyle style,
TextStyle? linkStyle,
}) {
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) {
return SelectableLinkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
return Linkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
static Future<void> handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog
final shouldOpen = await showDialog<bool>(
+31
View File
@@ -0,0 +1,31 @@
import '../models/contact.dart';
import '../connector/meshcore_protocol.dart';
class PathHelper {
static String formatPathHex(List<int> pathBytes) {
return pathBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(',');
}
static String resolvePathNames(
List<int> pathBytes,
List<Contact> allContacts,
) {
return pathBytes
.map((b) {
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
final matches = allContacts
.where(
(c) =>
c.publicKey.first == b &&
(c.type == advTypeRepeater || c.type == advTypeRoom),
)
.toList();
if (matches.isEmpty) return hex;
if (matches.length == 1) return matches.first.name;
return matches.map((c) => c.name).join(' | ');
})
.join(' \u2192 ');
}
}
+44
View File
@@ -8,6 +8,50 @@ class ReactionInfo {
}
class ReactionHelper {
/// Apply a reaction to a list of messages by matching the reaction hash.
///
/// [messages] - the message list to search
/// [reactionInfo] - the parsed reaction
/// [getTimestampSecs] - extract timestamp seconds from a message
/// [getSenderName] - extract sender name for hash (null for 1:1 implicit)
/// [getMessageText] - extract message text
/// [getReactions] - extract current reactions map
/// [shouldSkip] - filter function to skip messages (e.g., skip outgoing for incoming reactions)
/// [updateMessage] - callback to update the message at index with new reactions
///
/// Returns whether a match was found.
static bool applyReaction<T>({
required List<T> messages,
required ReactionInfo reactionInfo,
required int Function(T) getTimestampSecs,
required String? Function(T) getSenderName,
required String Function(T) getMessageText,
required Map<String, int> Function(T) getReactions,
required bool Function(T) shouldSkip,
required void Function(int index, Map<String, int> newReactions)
updateMessage,
}) {
final targetHash = reactionInfo.targetHash;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
if (shouldSkip(msg)) continue;
final msgHash = computeReactionHash(
getTimestampSecs(msg),
getSenderName(msg),
getMessageText(msg),
);
if (msgHash == targetHash) {
final currentReactions = Map<String, int>.from(getReactions(msg));
currentReactions[reactionInfo.emoji] =
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
updateMessage(i, currentReactions);
return true;
}
}
return false;
}
static List<String>? _cachedEmojis;
/// Combined list of all reaction emojis in fixed order.
+222 -2
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Нова група",
"contacts_groupName": "Група",
"contacts_groupNameRequired": "Името на групата е задължително.",
"contacts_groupNameReserved": "Това име на група е запазено",
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1606,6 +1607,8 @@
"scanner_bluetoothOff": "Bluetooth е изключен.",
"scanner_enableBluetooth": "Активирайте Bluetooth",
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
"scanner_chromeRequired": "Изисква се браузър Chrome",
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
"snrIndicator_lastSeen": "Последно видян",
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
"chat_ShowAllPaths": "Покажи всички пътища",
@@ -1799,5 +1802,222 @@
"contacts_unread": "Непрочетено",
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
"contacts_searchContactsNoNumber": "Търси контакти...",
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
}
"contacts_searchUsers": "Търсене на {number}{str} потребители...",
"contactsSettings_title": "Настройки на контактите",
"contactsSettings_autoAddTitle": "Автоматично откриване",
"contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители",
"contactsSettings_otherTitle": "Други настройки свързани с контакти",
"settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.",
"settings_contactSettings": "Настройки за контакти",
"contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици",
"contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите",
"contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.",
"contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи",
"contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.",
"contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.",
"contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.",
"contactsSettings_overwriteOldestTitle": "Премахни най-старото",
"discoveredContacts_Title": "Открити контакти",
"discoveredContacts_searchHint": "Търсене на открити контакти",
"discoveredContacts_noMatching": "Няма съвпадащи контакти",
"discoveredContacts_contactAdded": "Контакт добавен",
"discoveredContacts_copyContact": "Копирай контакт в клипборда",
"discoveredContacts_deleteContact": "Изтрий контакт",
"discoveredContacts_addContact": "Добави контакт",
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
"common_deleteAll": "Изтрий всичко",
"map_guessedLocation": "Предполагано местоположение",
"map_showGuessedLocations": "Покажете местоположенията на предположените възли.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Свържете се чрез USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.",
"usbScreenStatus": "Изберете USB устройство",
"usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.",
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.",
"usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.",
"usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.",
"usbErrorInvalidPort": "Изберете валитно USB устройство.",
"usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.",
"usbErrorNotConnected": "Няма свързано USB устройство.",
"usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.",
"usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.",
"usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.",
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
"usbErrorPortClosed": "USB връзката не е активна.",
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Свързване към USB устройство...",
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
"usbStatus_notConnected": "Изберете USB устройство",
"usbStatus_searching": "Търсене на USB устройства...",
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Свържете се чрез TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP адрес",
"tcpPortLabel": "Пристанище",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Въведете крайната точка и свържете се.",
"tcpStatus_connectingTo": "Свързване към {endpoint}...",
"tcpErrorHostRequired": "Необходим е IP адрес.",
"tcpErrorPortInvalid": "Портът трябва да бъде между 1 и 65535.",
"tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.",
"tcpErrorTimedOut": "Връзката TCP изтекла.",
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
"map_showDiscoveryContacts": "Покажи контакти за откриване",
"map_setAsMyLocation": "Задайте като моя местоположение",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_denyAll": "Откажи всичко",
"settings_allowAll": "Позволи всичко",
"settings_allowByContact": "Позволи по флагове за контакт",
"settings_privacy": "Настройки на поверителността",
"settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.",
"settings_privacySubtitle": "Контролирайте каква информация се споделя.",
"settings_telemetryBaseMode": "Базов режим на телеметрия",
"settings_telemetryLocationMode": "Режим на местоположение на телеметрията",
"settings_advertLocation": "Място на обявата",
"settings_advertLocationSubtitle": "Включи местоположение в обявата",
"contact_info": "Контактна информация",
"settings_telemetryEnvironmentMode": "Режим на средата на телеметрията",
"contact_telemetry": "Телеметрия",
"contact_lastSeen": "Последно видян",
"contact_clearChat": "Изчисти чата",
"contact_teleBase": "Базата данни за телеметрия",
"contact_settings": "Настройки за контакти",
"contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия",
"contact_teleEnv": "Среда на телеметрия",
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
"contact_teleLoc": "Местоположение на телеметрията",
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
"appSettings_maxRouteWeightSubtitle": "Максималното тегло, което един маршрут може да събере от успешни доставки.",
"appSettings_routeWeightSuccessIncrement": "Увеличение на теглото за успех",
"appSettings_routeWeightSuccessIncrementSubtitle": "Тегло, добавено към път след успешно доставяне.",
"appSettings_routeWeightFailureDecrement": "Намаляване на теглото, свързано с неуспех",
"appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.",
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Мулти-потвърди: {value}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
"map_showOverlaps": "Покриване на ключа на повтаряча",
"map_runTraceWithReturnPath": "Върни се по същия път.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Моля, изчакайте малко, преди да изпратите отново.",
"appSettings_languageHu": "Унгарски",
"appSettings_jumpToOldestUnread": "Преминете към най-старата непочетена статия",
"appSettings_jumpToOldestUnreadSubtitle": "Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.",
"appSettings_languageJa": "Японски",
"appSettings_languageKo": "Корейски",
"radioStats_tooltip": "Статистика за радио и мрежа",
"radioStats_screenTitle": "Статистически данни за радиопредаванията",
"radioStats_notConnected": "Свържете се с устройство, за да видите статистически данни за радиопредаване.",
"radioStats_firmwareTooOld": "Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.",
"radioStats_waiting": "Изчакване на данни…",
"radioStats_noiseFloor": "Ниво на шума: {noiseDbm} dBm",
"radioStats_lastRssi": "Последен RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Последна стойност на SNR: {snr} dB",
"radioStats_txAir": "Време на въздух (общо): {seconds} секунди",
"radioStats_rxAir": "Общо време на използване на RX (в секунди): {seconds} с",
"radioStats_chartCaption": "Ниво на шума (dBm) за последните измервания.",
"radioStats_stripNoise": "Ниво на шума: {noiseDbm} dBm",
"radioStats_stripWaiting": "Извличане на данни за радиото…",
"radioStats_settingsTile": "Статистически данни за радиостанции",
"radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingHidePin": "Скрий ПИН",
"scanner_linuxPairingShowPin": "Покажи PIN",
"scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth",
"scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма).",
"repeater_cliQuickClockSync": "Синхронизация на часовника",
"repeater_cliQuickDiscovery": "Открий Съседи"
}
+228 -8
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Neue Gruppe",
"contacts_groupName": "Gruppenname",
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
"contacts_groupNameReserved": "Dieser Gruppenname ist reserviert",
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -296,8 +297,8 @@
"contacts_filterContacts": "Filtert Kontakte...",
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
"contacts_noMembers": "Keine Mitglieder",
"contacts_lastSeenNow": "gerade gesehen",
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
"contacts_lastSeenNow": "kürzlich",
"contacts_lastSeenMinsAgo": "~ {minutes} Min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +306,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
"contacts_lastSeenHourAgo": "~ 1 Std.",
"contacts_lastSeenHoursAgo": "~ {hours} Std.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +315,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag",
"contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor",
"contacts_lastSeenDayAgo": "~ 1 Tag",
"contacts_lastSeenDaysAgo": "~ {days} Tage",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1635,6 +1636,8 @@
"pathTrace_clearTooltip": "Pfad löschen",
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
"scanner_chromeRequired": "Chrome Browser erforderlich",
"scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.",
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
"scanner_enableBluetooth": "Bluetooth aktivieren",
"snrIndicator_lastSeen": "Zuletzt gesehen",
@@ -1827,5 +1830,222 @@
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
}
"contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
"settings_contactSettings": "Kontakteinstellungen",
"contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten",
"contactsSettings_title": "Kontakteinstellungen",
"contactsSettings_autoAddTitle": "Automatische Erkennung",
"contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern",
"settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten",
"contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen",
"contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen",
"contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen",
"contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen",
"contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen",
"contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.",
"discoveredContacts_noMatching": "Keine passenden Kontakte",
"discoveredContacts_searchHint": "Entdeckte Kontakte suchen",
"discoveredContacts_addContact": "Kontakt hinzufügen",
"discoveredContacts_contactAdded": "Kontakt hinzugefügt",
"discoveredContacts_deleteContact": "Kontakt löschen",
"discoveredContacts_Title": "Entdeckte Kontakte",
"discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren",
"contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten",
"contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen",
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
"common_deleteAll": "Alles löschen",
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
"map_guessedLocation": "Geschätzter Ort",
"usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Verbinden über USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Wählen Sie ein USB-Gerät aus",
"usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.",
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.",
"usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.",
"usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.",
"usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.",
"usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.",
"usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.",
"usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.",
"usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.",
"usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.",
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
"usbFallbackDeviceName": "Web-Serielle Geräte",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Suche nach USB-Geräten...",
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "IP-Adresse",
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Verbinden über TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.",
"tcpStatus_connectingTo": "Verbindung zu {endpoint}...",
"tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.",
"tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.",
"tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.",
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
"map_setAsMyLocation": "Als meine aktuelle Position festlegen",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_allowByContact": "Zulassen durch Kontaktflaggen",
"settings_privacy": "Datenschutzeinstellungen",
"settings_allowAll": "Alles zulassen",
"settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.",
"settings_denyAll": "Alle ablehnen",
"settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.",
"settings_telemetryLocationMode": "Telemetrie-Ortsmodus",
"settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus",
"settings_advertLocation": "Anzeigenort",
"settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen",
"settings_telemetryBaseMode": "Telemetrie-Basismodus",
"contact_teleBase": "Telemetriebasis",
"contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie",
"contact_teleLoc": "Telemetrieort",
"contact_teleLocSubtitle": "Teilen von Standortdaten zulassen",
"contact_info": "Kontaktinformationen",
"contact_settings": "Kontakteinstellungen",
"contact_telemetry": "Telemetrie",
"contact_teleEnv": "Telemetrieumgebung",
"contact_lastSeen": "Zuletzt gesehen",
"contact_clearChat": "Chat löschen",
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
"@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",
"appSettings_initialRouteWeight": "Anfangs-Streckengewicht",
"appSettings_routeWeightSuccessIncrement": "Erhöhung des Erfolgsgewichts",
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.",
"appSettings_routeWeightFailureDecrement": "Reduzierung des Gewichts bei Fehlern",
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde",
"appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen",
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
"map_showOverlaps": "Überlappungen der Repeater-Taste",
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Bitte warten Sie einen Moment, bevor Sie erneut senden.",
"appSettings_jumpToOldestUnread": "Zum ältesten, nicht gelesenen Eintrag springen",
"appSettings_languageHu": "Ungarisch",
"appSettings_jumpToOldestUnreadSubtitle": "Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.",
"appSettings_languageJa": "Japanisch",
"appSettings_languageKo": "Koreanisch",
"radioStats_tooltip": "Daten zu Radio- und Mesh-Netzwerken",
"radioStats_screenTitle": "Senderinformationen",
"radioStats_notConnected": "Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.",
"radioStats_firmwareTooOld": "Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.",
"radioStats_waiting": "Warte auf Daten…",
"radioStats_noiseFloor": "Rauschpegel: {noiseDbm} dBm",
"radioStats_lastRssi": "Letzter RSSI-Wert: {rssiDbm} dBm",
"radioStats_lastSnr": "Letzter SNR: {snr} dB",
"radioStats_txAir": "Gesamt-TX-Zeit: {seconds} s",
"radioStats_rxAir": "Gesamt-RX-Zeit: {seconds} s",
"radioStats_chartCaption": "Rauschpegel (dBm) basierend auf den letzten Messwerten.",
"radioStats_stripNoise": "Rauschpegel: {noiseDbm} dBm",
"radioStats_stripWaiting": "Abrufen von Radiostatus…",
"radioStats_settingsTile": "Senderinformationen",
"radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"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"
}
+230 -9
View File
@@ -10,6 +10,7 @@
"common_unknownDevice": "Unknown Device",
"common_save": "Save",
"common_delete": "Delete",
"common_deleteAll": "Delete All",
"common_close": "Close",
"common_edit": "Edit",
"common_add": "Add",
@@ -46,6 +47,64 @@
}
},
"scanner_title": "MeshCore Open",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Connect over TCP",
"tcpHostLabel": "IP Address",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Enter endpoint and connect",
"tcpStatus_connectingTo": "Connecting to {endpoint}...",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"tcpErrorHostRequired": "IP address is required.",
"tcpErrorPortInvalid": "Port must be between 1 and 65535.",
"tcpErrorUnsupported": "TCP transport is not supported on this platform.",
"tcpErrorTimedOut": "TCP connection timed out.",
"tcpConnectionFailed": "TCP connection failed: {error}",
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbScreenTitle": "Connect over USB",
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
"usbScreenStatus": "Select a USB device",
"usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
"usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
"usbErrorPermissionDenied": "USB permission was denied.",
"usbErrorDeviceMissing": "The selected USB device is no longer available.",
"usbErrorInvalidPort": "Select a valid USB device.",
"usbErrorBusy": "Another USB connection request is already in progress.",
"usbErrorNotConnected": "No USB device is connected.",
"usbErrorOpenFailed": "Failed to open the selected USB device.",
"usbErrorConnectFailed": "Failed to connect to the selected USB device.",
"usbErrorUnsupported": "USB serial is not supported on this platform.",
"usbErrorAlreadyActive": "A USB connection is already active.",
"usbErrorNoDeviceSelected": "No USB device was selected.",
"usbErrorPortClosed": "The USB connection is not open.",
"usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.",
"usbFallbackDeviceName": "Web Serial Device",
"usbStatus_notConnected": "Select a USB device",
"usbStatus_connecting": "Connecting to USB device...",
"usbStatus_searching": "Searching for USB devices...",
"usbConnectionFailed": "USB connection failed: {error}",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"scanner_scanning": "Scanning for devices...",
"scanner_connecting": "Connecting...",
"scanner_disconnecting": "Disconnecting...",
@@ -68,10 +127,13 @@
}
}
},
"scanner_stop": "Stop",
"scanner_scan": "Scan",
"scanner_bluetoothOff": "Bluetooth is off",
"scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices",
"scanner_chromeRequired": "Chrome Browser Required",
"scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.",
"scanner_enableBluetooth": "Enable Bluetooth",
"device_quickSwitch": "Quick switch",
"device_meshcore": "MeshCore",
@@ -98,11 +160,33 @@
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_contactSettings": "Contact Settings",
"settings_contactSettingsSubtitle": "Settings for how contacts are added.",
"settings_privacyMode": "Privacy Mode",
"settings_privacyModeSubtitle": "Hide name/location in advertisements",
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
"settings_privacyModeEnabled": "Privacy mode enabled",
"settings_privacyModeDisabled": "Privacy mode disabled",
"settings_privacy": "Privacy Settings",
"settings_privacySubtitle": "Control what information is shared.",
"settings_privacySettingsDescription": "Choose what information your device shares with others.",
"settings_denyAll": "Deny all",
"settings_allowByContact": "Allow by contact flags",
"settings_allowAll": "Allow all",
"settings_telemetryBaseMode": "Telemetry Base Mode",
"settings_telemetryLocationMode": "Telemetry Location Mode",
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.",
"settings_multiAck": "Multi-ACKs: {value}",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions",
"settings_sendAdvertisement": "Send Advertisement",
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
@@ -206,6 +290,27 @@
"appSettings_autoRouteRotationSubtitle": "Cycle between best paths and flood mode",
"appSettings_autoRouteRotationEnabled": "Auto route rotation enabled",
"appSettings_autoRouteRotationDisabled": "Auto route rotation disabled",
"appSettings_maxRouteWeight": "Max Route Weight",
"appSettings_maxRouteWeightSubtitle": "Maximum weight a path can accumulate from successful deliveries",
"appSettings_initialRouteWeight": "Initial Route Weight",
"appSettings_initialRouteWeightSubtitle": "Starting weight for newly discovered paths",
"appSettings_routeWeightSuccessIncrement": "Success Weight Increment",
"appSettings_routeWeightSuccessIncrementSubtitle": "Weight added to a path after successful delivery",
"appSettings_routeWeightFailureDecrement": "Failure Weight Decrement",
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
"appSettings_maxMessageRetries": "Max Message Retries",
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
"path_routeWeight": "{weight}/{max}",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"appSettings_battery": "Battery",
"appSettings_batteryChemistry": "Battery Chemistry",
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
@@ -353,6 +458,7 @@
"contacts_newGroup": "New Group",
"contacts_groupName": "Group name",
"contacts_groupNameRequired": "Group name is required",
"contacts_groupNameReserved": "This group name is reserved",
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -364,8 +470,8 @@
"contacts_filterContacts": "Filter contacts...",
"contacts_noContactsMatchFilter": "No contacts match your filter",
"contacts_noMembers": "No members",
"contacts_lastSeenNow": "Last seen now",
"contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago",
"contacts_lastSeenNow": "recently",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -373,8 +479,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Last seen 1 hour ago",
"contacts_lastSeenHoursAgo": "Last seen {hours} hours ago",
"contacts_lastSeenHourAgo": "~ 1 hour",
"contacts_lastSeenHoursAgo": "~ {hours} hours",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -382,8 +488,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Last seen 1 day ago",
"contacts_lastSeenDaysAgo": "Last seen {days} days ago",
"contacts_lastSeenDayAgo": "~ 1 day",
"contacts_lastSeenDaysAgo": "~ {days} days",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -391,6 +497,17 @@
}
}
},
"contact_info": "Contact Info",
"contact_settings": "Contact Settings",
"contact_telemetry": "Telemetry",
"contact_lastSeen": "Last seen",
"contact_clearChat": "Clear Chat",
"contact_teleBase": "Telemetry Base",
"contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry",
"contact_teleLoc": "Telemetry Location",
"contact_teleLocSubtitle": "Allow sharing location data",
"contact_teleEnv": "Telemetry Environment",
"contact_teleEnvSubtitle": "Allow sharing environment sensor data",
"channels_title": "Channels",
"channels_noChannelsConfigured": "No channels configured",
"channels_addPublicChannel": "Add Public Channel",
@@ -744,6 +861,7 @@
"map_source": "Source",
"map_flags": "Flags",
"map_shareMarkerHere": "Share marker here",
"map_setAsMyLocation": "Set as my location",
"map_pinLabel": "Pin label",
"map_label": "Label",
"map_pointOfInterest": "Point of interest",
@@ -765,17 +883,22 @@
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Other Nodes",
"map_showOverlaps": "Repeater Key Overlaps",
"map_keyPrefix": "Key Prefix",
"map_filterByKeyPrefix": "Filter by key prefix",
"map_publicKeyPrefix": "Public key prefix",
"map_markers": "Markers",
"map_showSharedMarkers": "Show shared markers",
"map_showGuessedLocations": "Show guessed node locations",
"map_showDiscoveryContacts": "Show Discovery Contacts",
"map_guessedLocation": "Guessed location",
"map_lastSeenTime": "Last Seen Time",
"map_sharedPin": "Shared pin",
"map_joinRoom": "Join Room",
"map_manageRepeater": "Manage Repeater",
"map_tapToAdd": "Tap on nodes to add them to the path.",
"map_runTrace": "Run Path Trace",
"map_runTrace": "Run path trace",
"map_runTraceWithReturnPath": "Return back on the same path.",
"map_removeLast": "Remove Last",
"map_pathTraceCancelled": "Path trace cancelled.",
"mapCache_title": "Offline Map Cache",
@@ -1214,6 +1337,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.",
@@ -1837,5 +1962,101 @@
"settings_gpxExportShareText": "Map data exported from meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open GPX map data export",
"snrIndicator_nearByRepeaters": "Nearby Repeaters",
"snrIndicator_lastSeen": "Last seen"
}
"snrIndicator_lastSeen": "Last seen",
"contactsSettings_title": "Contacts settings",
"contactsSettings_autoAddTitle": "Automatic Discovery",
"contactsSettings_otherTitle": "Other contact related settings",
"contactsSettings_autoAddUsersTitle": "Auto-add users",
"contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.",
"contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters",
"contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.",
"contactsSettings_autoAddRoomServersTitle": "Auto-add room servers",
"contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.",
"contactsSettings_autoAddSensorsTitle": "Auto-add sensors",
"contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.",
"contactsSettings_overwriteOldestTitle": "Overwrite Oldest",
"contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.",
"discoveredContacts_Title": "Discovered Contacts",
"discoveredContacts_noMatching": "No matching contacts",
"discoveredContacts_searchHint": "Search discovered contacts",
"discoveredContacts_contactAdded": "Contact added",
"discoveredContacts_addContact": "Add Contact",
"discoveredContacts_copyContact": "Copy Contact to clipboard",
"discoveredContacts_deleteContact": "Delete Discovered Contact",
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?",
"chat_sendCooldown": "Please wait a moment before sending again.",
"appSettings_jumpToOldestUnread": "Jump to oldest unread",
"appSettings_jumpToOldestUnreadSubtitle": "When opening a chat with unread messages, scroll to the first unread instead of the latest.",
"appSettings_languageHu": "Hungarian",
"appSettings_languageJa": "Japanese",
"appSettings_languageKo": "Korean",
"radioStats_tooltip": "Radio & mesh stats",
"radioStats_screenTitle": "Radio stats",
"radioStats_notConnected": "Connect to a device to view radio statistics.",
"radioStats_firmwareTooOld": "Radio statistics require companion firmware v8 or newer.",
"radioStats_waiting": "Waiting for data…",
"radioStats_noiseFloor": "Noise floor: {noiseDbm} dBm",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"radioStats_lastRssi": "Last RSSI: {rssiDbm} dBm",
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"radioStats_lastSnr": "Last SNR: {snr} dB",
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"radioStats_txAir": "TX airtime (total): {seconds} s",
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"radioStats_rxAir": "RX airtime (total): {seconds} s",
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"radioStats_chartCaption": "Noise floor (dBm) over recent samples.",
"radioStats_stripNoise": "Noise floor: {noiseDbm} dBm",
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"radioStats_stripWaiting": "Fetching radio stats…",
"radioStats_settingsTile": "Radio stats",
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
"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"
}
}
}
}
+227 -7
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nuevo Grupo",
"contacts_groupName": "Nombre del grupo",
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
"contacts_groupNameReserved": "Este nombre de grupo está reservado",
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -297,7 +298,7 @@
"contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro",
"contacts_noMembers": "No miembros",
"contacts_lastSeenNow": "Última vez que se vio ahora",
"contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +306,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora",
"contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.",
"contacts_lastSeenHourAgo": "~ 1 hora",
"contacts_lastSeenHoursAgo": "~ {hours} horas",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +315,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día",
"contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.",
"contacts_lastSeenDayAgo": "~ 1 día",
"contacts_lastSeenDaysAgo": "~ {days} días",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1632,6 +1633,8 @@
"map_removeLast": "Eliminar último",
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
"scanner_chromeRequired": "Navegador Chrome requerido",
"scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.",
"scanner_bluetoothOff": "Bluetooth está desactivado.",
"scanner_enableBluetooth": "Habilitar Bluetooth",
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
@@ -1827,5 +1830,222 @@
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
}
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
"contactsSettings_autoAddTitle": "Detección automática",
"settings_contactSettings": "Configuración de contacto",
"contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente",
"contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto",
"contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.",
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.",
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.",
"contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente",
"contactsSettings_title": "Configuración de contactos",
"settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.",
"contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.",
"contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente",
"contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo",
"contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala",
"discoveredContacts_noMatching": "No se encontraron contactos coincidentes",
"discoveredContacts_contactAdded": "Contacto agregado",
"discoveredContacts_copyContact": "Copiar contacto al portapapeles",
"discoveredContacts_deleteContact": "Eliminar contacto",
"discoveredContacts_Title": "Contactos descubiertos",
"discoveredContacts_searchHint": "Buscar contactos descubiertos",
"discoveredContacts_addContact": "Agregar contacto",
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
"common_deleteAll": "Eliminar todo",
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
"map_guessedLocation": "Ubicación estimada",
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.",
"usbScreenTitle": "Conecte mediante USB",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.",
"usbScreenStatus": "Seleccione un dispositivo USB",
"usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.",
"usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.",
"usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.",
"usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.",
"usbErrorInvalidPort": "Seleccione un dispositivo USB válido.",
"usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.",
"usbErrorNotConnected": "No hay ningún dispositivo USB conectado.",
"usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.",
"usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.",
"usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.",
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
"usbErrorPortClosed": "La conexión USB no está activa.",
"usbFallbackDeviceName": "Dispositivo de serie web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Conectándose al dispositivo USB...",
"usbStatus_searching": "Buscando dispositivos USB...",
"usbStatus_notConnected": "Seleccione un dispositivo USB",
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpScreenTitle": "Establecer conexión a través de TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "Dirección IP",
"tcpPortLabel": "Puerto",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Ingrese la dirección final y conecte.",
"tcpStatus_connectingTo": "Conectándose a {endpoint}...",
"tcpErrorHostRequired": "Se requiere la dirección IP.",
"tcpErrorPortInvalid": "El puerto debe estar entre 1 y 65535.",
"tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.",
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
"map_setAsMyLocation": "Establecer mi ubicación",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySubtitle": "Controlar qué información se comparte.",
"settings_allowByContact": "Permitir por banderas de contacto",
"settings_denyAll": "Denegar todo",
"settings_telemetryBaseMode": "Modo base de telemetría",
"settings_telemetryEnvironmentMode": "Modo de entorno de telemetría",
"settings_advertLocationSubtitle": "Incluir ubicación en anuncio",
"contact_info": "Información de contacto",
"settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.",
"settings_allowAll": "Permitir todo",
"settings_privacy": "Configuración de privacidad",
"contact_settings": "Configuración de contacto",
"settings_telemetryLocationMode": "Modo de ubicación de telemetría",
"contact_teleBase": "Base de Telemetría",
"contact_teleLoc": "Ubicación de telemetría",
"settings_advertLocation": "Ubicación de anuncio",
"contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación",
"contact_clearChat": "Borrar chat",
"contact_telemetry": "Telemetría",
"contact_lastSeen": "Visto por última vez",
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
"contact_teleEnv": "Entorno de Telemetría",
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
"@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",
"appSettings_maxRouteWeightSubtitle": "Peso máximo que una ruta puede acumular gracias a entregas exitosas.",
"appSettings_routeWeightSuccessIncrement": "Incremento de peso para el éxito",
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso añadido a una ruta después de una entrega exitosa.",
"appSettings_routeWeightFailureDecrement": "Reducción del peso asociado al fallo",
"appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.",
"appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes",
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Superposiciones de tecla repetidora",
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Salta a los mensajes más antiguos sin leer",
"chat_sendCooldown": "Por favor, espere un momento antes de reenviar.",
"appSettings_languageHu": "Húngaro",
"appSettings_jumpToOldestUnreadSubtitle": "Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.",
"appSettings_languageJa": "Japonés",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Estadísticas de radio y malla",
"radioStats_screenTitle": "Estadísticas de radio",
"radioStats_notConnected": "Conéctese a un dispositivo para visualizar estadísticas de radio.",
"radioStats_firmwareTooOld": "Las estadísticas de radio requieren un firmware compatible v8 o posterior.",
"radioStats_waiting": "Esperando datos…",
"radioStats_noiseFloor": "Nivel de ruido: {noiseDbm} dBm",
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Último SNR: {snr} dB",
"radioStats_txAir": "Tiempo de emisión en Texas (total): {seconds} s",
"radioStats_rxAir": "Tiempo de transmisión de RX (total): {seconds} s",
"radioStats_chartCaption": "Nivel de ruido (dBm) en muestras recientes.",
"radioStats_stripNoise": "Nivel de ruido: {noiseDbm} dBm",
"radioStats_stripWaiting": "Obteniendo estadísticas de la radio…",
"radioStats_settingsTile": "Estadísticas de radio",
"radioStats_settingsSubtitle": "Nivel de ruido, RSSI, SNR y tiempo de transmisión",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Mostrar PIN",
"scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth",
"scanner_linuxPairingHidePin": "Ocultar PIN",
"scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno).",
"repeater_cliQuickDiscovery": "Descubrir Vecinos",
"repeater_cliQuickClockSync": "Sincronización del reloj"
}
+227 -7
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nouveau Groupe",
"contacts_groupName": "Nom du groupe",
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
"contacts_groupNameReserved": "Ce nom de groupe est réservé",
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -297,7 +298,7 @@
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
"contacts_noMembers": "Aucun membre",
"contacts_lastSeenNow": "Vu maintenant",
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +306,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
"contacts_lastSeenHourAgo": "~ 1 heure",
"contacts_lastSeenHoursAgo": "~ {hours} heures",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +315,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
"contacts_lastSeenDayAgo": "~ 1 jour",
"contacts_lastSeenDaysAgo": "~ {days} jours",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -1604,6 +1605,8 @@
"map_removeLast": "Supprimer le dernier",
"map_runTrace": "Exécuter la traçage de chemin",
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
"scanner_chromeRequired": "Navigateur Chrome requis",
"scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.",
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
"scanner_enableBluetooth": "Activer le Bluetooth",
"snrIndicator_lastSeen": "Dernière fois vu",
@@ -1799,5 +1802,222 @@
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
}
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
"settings_contactSettings": "Paramètres de contact",
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
"contactsSettings_title": "Paramètres des contacts",
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
"contactsSettings_autoAddTitle": "Découverte automatique",
"contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs",
"contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts",
"discoveredContacts_noMatching": "Aucun contact correspondant",
"discoveredContacts_contactAdded": "Contact ajouté",
"discoveredContacts_addContact": "Ajouter un contact",
"discoveredContacts_copyContact": "Copier le contact dans le presse-papiers",
"discoveredContacts_deleteContact": "Supprimer le contact",
"contactsSettings_overwriteOldestTitle": "Écraser le plus ancien",
"contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.",
"discoveredContacts_Title": "Contacts découverts",
"discoveredContacts_searchHint": "Rechercher des contacts découverts",
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
"common_deleteAll": "Supprimer tout",
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
"map_guessedLocation": "Lieu deviné",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Connectez via USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.",
"usbScreenStatus": "Sélectionnez un périphérique USB",
"usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.",
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.",
"usbErrorPermissionDenied": "L'accès via USB a été refusé.",
"usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.",
"usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.",
"usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.",
"usbErrorNotConnected": "Aucun appareil USB n'est connecté.",
"usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.",
"usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.",
"usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.",
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
"usbFallbackDeviceName": "Dispositif de communication série sur le Web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_notConnected": "Sélectionnez un périphérique USB",
"usbConnectionFailed": "Échec de la connexion USB : {error}",
"usbStatus_connecting": "Connexion au périphérique USB...",
"usbStatus_searching": "Recherche de périphériques USB...",
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Adresse IP",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Établir une connexion via TCP",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Entrez l'adresse de destination et connectez-vous.",
"tcpStatus_connectingTo": "Connexion à {endpoint}...",
"tcpErrorHostRequired": "Une adresse IP est obligatoire.",
"tcpErrorPortInvalid": "La taille du port doit être comprise entre 1 et 65535.",
"tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.",
"tcpErrorTimedOut": "La connexion TCP a expiré.",
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
"map_showDiscoveryContacts": "Afficher les contacts de découverte",
"map_setAsMyLocation": "Définir comme ma localisation",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Paramètres de confidentialité",
"settings_privacySubtitle": "Contrôlez les informations partagées",
"settings_telemetryLocationMode": "Mode d'emplacement de télémétrie",
"settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie",
"settings_advertLocation": "Emplacement de l'annonce",
"settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce",
"settings_denyAll": "Refuser tout",
"settings_allowByContact": "Autoriser par drapeaux de contact",
"settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.",
"settings_allowAll": "Autoriser tout",
"contact_info": "Informations de contact",
"settings_telemetryBaseMode": "Mode de base Télémétrie",
"contact_teleBase": "Base de télémétrie",
"contact_teleLoc": "Emplacement de télémétrie",
"contact_teleLocSubtitle": "Autoriser le partage des données de localisation",
"contact_teleEnv": "Environnement Télémétrie",
"contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement",
"contact_telemetry": "Télémétrie",
"contact_settings": "Paramètres de contact",
"contact_lastSeen": "Dernière fois vu",
"contact_clearChat": "Effacer la conversation",
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
"@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",
"appSettings_initialRouteWeightSubtitle": "Poids de départ pour les nouveaux chemins découverts",
"appSettings_routeWeightSuccessIncrement": "Augmentation du poids de réussite",
"appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un itinéraire après une livraison réussie.",
"appSettings_routeWeightFailureDecrement": "Réduction du poids de pénalité",
"appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.",
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
"path_routeWeight": "{weight}/{max}",
"settings_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.",
"@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",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Afficher le code PIN",
"scanner_linuxPairingHidePin": "Masquer le code PIN",
"scanner_linuxPairingPinTitle": "Code PIN dappairage Bluetooth",
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun).",
"repeater_cliQuickClockSync": "Synchronisation de l'horloge",
"repeater_cliQuickDiscovery": "Découvrir les voisins"
}
+2061
View File
File diff suppressed because it is too large Load Diff
+222 -2
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nuovo Gruppo",
"contacts_groupName": "Nome gruppo",
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
"contacts_groupNameReserved": "Questo nome del gruppo è riservato",
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1606,8 @@
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
"scanner_chromeRequired": "Browser Chrome richiesto",
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
"scanner_enableBluetooth": "Abilita il Bluetooth",
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
"snrIndicator_lastSeen": "Ultimo accesso",
@@ -1799,5 +1802,222 @@
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
"contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
}
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
"contactsSettings_title": "Impostazioni dei contatti",
"settings_contactSettings": "Impostazioni di contatto",
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
"contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.",
"contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente",
"contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.",
"contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori",
"settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti",
"contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente",
"contactsSettings_autoAddTitle": "Scoperta automatica",
"contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti",
"discoveredContacts_noMatching": "Nessun contatto corrispondente",
"contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.",
"discoveredContacts_searchHint": "Cerca contatti scoperti",
"contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze",
"discoveredContacts_addContact": "Aggiungi contatto",
"contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio",
"discoveredContacts_Title": "Contatti scoperti",
"discoveredContacts_contactAdded": "Contatto aggiunto",
"discoveredContacts_deleteContact": "Elimina Contatto",
"discoveredContacts_copyContact": "Copia contatto negli appunti",
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
"common_deleteAll": "Elimina tutto",
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
"map_guessedLocation": "Località indovinata",
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "Connessione tramite USB",
"usbScreenStatus": "Seleziona un dispositivo USB",
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.",
"usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.",
"usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.",
"usbErrorInvalidPort": "Seleziona un dispositivo USB valido.",
"usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.",
"usbErrorNotConnected": "Non è collegato alcun dispositivo USB.",
"usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.",
"usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.",
"usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.",
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
"usbErrorPortClosed": "La connessione USB non è attiva.",
"usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Ricerca di dispositivi USB...",
"usbConnectionFailed": "Errore nella connessione USB: {error}",
"usbStatus_notConnected": "Seleziona un dispositivo USB",
"usbStatus_connecting": "Connessione al dispositivo USB...",
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Indirizzo IP",
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Stabilire una connessione tramite TCP",
"tcpPortLabel": "Porta",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Inserisci l'endpoint e connettiti.",
"tcpStatus_connectingTo": "Connessione a {endpoint}...",
"tcpErrorHostRequired": "È necessario fornire un indirizzo IP.",
"tcpErrorPortInvalid": "La dimensione della porta deve essere compresa tra 1 e 65535.",
"tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.",
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
"map_showDiscoveryContacts": "Mostra Contatti di Discovery",
"map_setAsMyLocation": "Imposta come la mia posizione",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.",
"settings_allowByContact": "Consenti in base ai flag di contatto",
"settings_telemetryLocationMode": "Modalità di posizionamento telemetrico",
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
"settings_advertLocation": "Posizione dell'annuncio",
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
"settings_privacy": "Impostazioni sulla privacy",
"settings_denyAll": "Negare tutto",
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
"settings_allowAll": "Consenti tutto",
"contact_info": "Informazioni di Contatto",
"settings_telemetryBaseMode": "Modalità di base di telemetria",
"contact_teleBase": "Base di telemetria",
"contact_teleLoc": "Posizione telemetria",
"contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione",
"contact_clearChat": "Cancella chat",
"contact_telemetry": "Telemetria",
"contact_settings": "Impostazioni di contatto",
"contact_lastSeen": "Ultimo accesso",
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
"contact_teleEnv": "Ambiente di telemetria",
"@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.",
"appSettings_maxRouteWeight": "Massimo peso consentito per il percorso",
"appSettings_routeWeightSuccessIncrement": "Aumento del peso del successo",
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso aggiunto a un percorso dopo una consegna riuscita.",
"appSettings_routeWeightFailureDecrement": "Riduzione del peso associato al fallimento",
"appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.",
"appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio",
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnreadSubtitle": "Quando si apre una chat con messaggi non letti, scorrete verso l'alto fino al primo messaggio non letto, invece che al più recente.",
"chat_sendCooldown": "Si prega di attendere un momento prima di inviare nuovamente.",
"appSettings_jumpToOldestUnread": "Vai al messaggio più vecchio non letto",
"appSettings_languageHu": "Ungherese",
"appSettings_languageJa": "Giapponese",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Statistiche per radio e reti",
"radioStats_screenTitle": "Statistiche radio",
"radioStats_notConnected": "Connettiti a un dispositivo per visualizzare le statistiche radio.",
"radioStats_firmwareTooOld": "Le statistiche radio richiedono il firmware versione 8 o successiva.",
"radioStats_noiseFloor": "Livello di rumore: {noiseDbm} dBm",
"radioStats_waiting": "In attesa dei dati…",
"radioStats_lastRssi": "Ultimo valore RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Ultimo SNR: {snr} dB",
"radioStats_txAir": "Tempo di trasmissione in diretta (totale): {seconds} s",
"radioStats_rxAir": "Tempo di trasmissione RX (totale): {seconds} s",
"radioStats_chartCaption": "Livello di rumore (dBm) misurato su campioni recenti.",
"radioStats_stripNoise": "Livello di rumore: {noiseDbm} dBm",
"radioStats_stripWaiting": "Recupero delle statistiche radio…",
"radioStats_settingsTile": "Statistiche radio",
"radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Mostra PIN",
"scanner_linuxPairingHidePin": "Nascondi PIN",
"scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth",
"scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è).",
"repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
"repeater_cliQuickDiscovery": "Scopri i Vicini"
}
+2061
View File
File diff suppressed because it is too large Load Diff
+2061
View File
File diff suppressed because it is too large Load Diff
+820 -7
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'),
@@ -184,6 +190,12 @@ abstract class AppLocalizations {
/// **'Delete'**
String get common_delete;
/// No description provided for @common_deleteAll.
///
/// In en, this message translates to:
/// **'Delete All'**
String get common_deleteAll;
/// No description provided for @common_close.
///
/// In en, this message translates to:
@@ -316,6 +328,228 @@ abstract class AppLocalizations {
/// **'MeshCore Open'**
String get scanner_title;
/// No description provided for @connectionChoiceUsbLabel.
///
/// In en, this message translates to:
/// **'USB'**
String get connectionChoiceUsbLabel;
/// No description provided for @connectionChoiceBluetoothLabel.
///
/// In en, this message translates to:
/// **'Bluetooth'**
String get connectionChoiceBluetoothLabel;
/// No description provided for @connectionChoiceTcpLabel.
///
/// In en, this message translates to:
/// **'TCP'**
String get connectionChoiceTcpLabel;
/// No description provided for @tcpScreenTitle.
///
/// In en, this message translates to:
/// **'Connect over TCP'**
String get tcpScreenTitle;
/// No description provided for @tcpHostLabel.
///
/// In en, this message translates to:
/// **'IP Address'**
String get tcpHostLabel;
/// No description provided for @tcpHostHint.
///
/// In en, this message translates to:
/// **'192.168.40.10'**
String get tcpHostHint;
/// No description provided for @tcpPortLabel.
///
/// In en, this message translates to:
/// **'Port'**
String get tcpPortLabel;
/// No description provided for @tcpPortHint.
///
/// In en, this message translates to:
/// **'5000'**
String get tcpPortHint;
/// No description provided for @tcpStatus_notConnected.
///
/// In en, this message translates to:
/// **'Enter endpoint and connect'**
String get tcpStatus_notConnected;
/// No description provided for @tcpStatus_connectingTo.
///
/// In en, this message translates to:
/// **'Connecting to {endpoint}...'**
String tcpStatus_connectingTo(String endpoint);
/// No description provided for @tcpErrorHostRequired.
///
/// In en, this message translates to:
/// **'IP address is required.'**
String get tcpErrorHostRequired;
/// No description provided for @tcpErrorPortInvalid.
///
/// In en, this message translates to:
/// **'Port must be between 1 and 65535.'**
String get tcpErrorPortInvalid;
/// No description provided for @tcpErrorUnsupported.
///
/// In en, this message translates to:
/// **'TCP transport is not supported on this platform.'**
String get tcpErrorUnsupported;
/// No description provided for @tcpErrorTimedOut.
///
/// In en, this message translates to:
/// **'TCP connection timed out.'**
String get tcpErrorTimedOut;
/// No description provided for @tcpConnectionFailed.
///
/// In en, this message translates to:
/// **'TCP connection failed: {error}'**
String tcpConnectionFailed(String error);
/// No description provided for @usbScreenTitle.
///
/// In en, this message translates to:
/// **'Connect over USB'**
String get usbScreenTitle;
/// No description provided for @usbScreenSubtitle.
///
/// In en, this message translates to:
/// **'Choose a detected serial device and connect directly to your MeshCore node.'**
String get usbScreenSubtitle;
/// No description provided for @usbScreenStatus.
///
/// In en, this message translates to:
/// **'Select a USB device'**
String get usbScreenStatus;
/// No description provided for @usbScreenNote.
///
/// In en, this message translates to:
/// **'USB serial is active on supported Android devices and desktop platforms.'**
String get usbScreenNote;
/// No description provided for @usbScreenEmptyState.
///
/// In en, this message translates to:
/// **'No USB devices found. Plug one in and refresh.'**
String get usbScreenEmptyState;
/// No description provided for @usbErrorPermissionDenied.
///
/// In en, this message translates to:
/// **'USB permission was denied.'**
String get usbErrorPermissionDenied;
/// No description provided for @usbErrorDeviceMissing.
///
/// In en, this message translates to:
/// **'The selected USB device is no longer available.'**
String get usbErrorDeviceMissing;
/// No description provided for @usbErrorInvalidPort.
///
/// In en, this message translates to:
/// **'Select a valid USB device.'**
String get usbErrorInvalidPort;
/// No description provided for @usbErrorBusy.
///
/// In en, this message translates to:
/// **'Another USB connection request is already in progress.'**
String get usbErrorBusy;
/// No description provided for @usbErrorNotConnected.
///
/// In en, this message translates to:
/// **'No USB device is connected.'**
String get usbErrorNotConnected;
/// No description provided for @usbErrorOpenFailed.
///
/// In en, this message translates to:
/// **'Failed to open the selected USB device.'**
String get usbErrorOpenFailed;
/// No description provided for @usbErrorConnectFailed.
///
/// In en, this message translates to:
/// **'Failed to connect to the selected USB device.'**
String get usbErrorConnectFailed;
/// No description provided for @usbErrorUnsupported.
///
/// In en, this message translates to:
/// **'USB serial is not supported on this platform.'**
String get usbErrorUnsupported;
/// No description provided for @usbErrorAlreadyActive.
///
/// In en, this message translates to:
/// **'A USB connection is already active.'**
String get usbErrorAlreadyActive;
/// No description provided for @usbErrorNoDeviceSelected.
///
/// In en, this message translates to:
/// **'No USB device was selected.'**
String get usbErrorNoDeviceSelected;
/// No description provided for @usbErrorPortClosed.
///
/// In en, this message translates to:
/// **'The USB connection is not open.'**
String get usbErrorPortClosed;
/// No description provided for @usbErrorConnectTimedOut.
///
/// In en, this message translates to:
/// **'Connection timed out. Make sure the device has USB Companion firmware.'**
String get usbErrorConnectTimedOut;
/// No description provided for @usbFallbackDeviceName.
///
/// In en, this message translates to:
/// **'Web Serial Device'**
String get usbFallbackDeviceName;
/// No description provided for @usbStatus_notConnected.
///
/// In en, this message translates to:
/// **'Select a USB device'**
String get usbStatus_notConnected;
/// No description provided for @usbStatus_connecting.
///
/// In en, this message translates to:
/// **'Connecting to USB device...'**
String get usbStatus_connecting;
/// No description provided for @usbStatus_searching.
///
/// In en, this message translates to:
/// **'Searching for USB devices...'**
String get usbStatus_searching;
/// No description provided for @usbConnectionFailed.
///
/// In en, this message translates to:
/// **'USB connection failed: {error}'**
String usbConnectionFailed(String error);
/// No description provided for @scanner_scanning.
///
/// In en, this message translates to:
@@ -388,6 +622,18 @@ abstract class AppLocalizations {
/// **'Please turn on Bluetooth to scan for devices'**
String get scanner_bluetoothOffMessage;
/// No description provided for @scanner_chromeRequired.
///
/// In en, this message translates to:
/// **'Chrome Browser Required'**
String get scanner_chromeRequired;
/// No description provided for @scanner_chromeRequiredMessage.
///
/// In en, this message translates to:
/// **'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'**
String get scanner_chromeRequiredMessage;
/// No description provided for @scanner_enableBluetooth.
///
/// In en, this message translates to:
@@ -544,6 +790,18 @@ abstract class AppLocalizations {
/// **'Longitude'**
String get settings_longitude;
/// No description provided for @settings_contactSettings.
///
/// In en, this message translates to:
/// **'Contact Settings'**
String get settings_contactSettings;
/// No description provided for @settings_contactSettingsSubtitle.
///
/// In en, this message translates to:
/// **'Settings for how contacts are added.'**
String get settings_contactSettingsSubtitle;
/// No description provided for @settings_privacyMode.
///
/// In en, this message translates to:
@@ -574,6 +832,84 @@ abstract class AppLocalizations {
/// **'Privacy mode disabled'**
String get settings_privacyModeDisabled;
/// No description provided for @settings_privacy.
///
/// In en, this message translates to:
/// **'Privacy Settings'**
String get settings_privacy;
/// No description provided for @settings_privacySubtitle.
///
/// In en, this message translates to:
/// **'Control what information is shared.'**
String get settings_privacySubtitle;
/// No description provided for @settings_privacySettingsDescription.
///
/// In en, this message translates to:
/// **'Choose what information your device shares with others.'**
String get settings_privacySettingsDescription;
/// No description provided for @settings_denyAll.
///
/// In en, this message translates to:
/// **'Deny all'**
String get settings_denyAll;
/// No description provided for @settings_allowByContact.
///
/// In en, this message translates to:
/// **'Allow by contact flags'**
String get settings_allowByContact;
/// No description provided for @settings_allowAll.
///
/// In en, this message translates to:
/// **'Allow all'**
String get settings_allowAll;
/// No description provided for @settings_telemetryBaseMode.
///
/// In en, this message translates to:
/// **'Telemetry Base Mode'**
String get settings_telemetryBaseMode;
/// No description provided for @settings_telemetryLocationMode.
///
/// In en, this message translates to:
/// **'Telemetry Location Mode'**
String get settings_telemetryLocationMode;
/// No description provided for @settings_telemetryEnvironmentMode.
///
/// In en, this message translates to:
/// **'Telemetry Environment Mode'**
String get settings_telemetryEnvironmentMode;
/// No description provided for @settings_advertLocation.
///
/// In en, this message translates to:
/// **'Advert Location'**
String get settings_advertLocation;
/// No description provided for @settings_advertLocationSubtitle.
///
/// In en, this message translates to:
/// **'Include location in advert.'**
String get settings_advertLocationSubtitle;
/// No description provided for @settings_multiAck.
///
/// In en, this message translates to:
/// **'Multi-ACKs: {value}'**
String settings_multiAck(String value);
/// No description provided for @settings_telemetryModeUpdated.
///
/// In en, this message translates to:
/// **'Telemetry mode updated'**
String get settings_telemetryModeUpdated;
/// No description provided for @settings_actions.
///
/// In en, this message translates to:
@@ -1108,6 +1444,72 @@ abstract class AppLocalizations {
/// **'Auto route rotation disabled'**
String get appSettings_autoRouteRotationDisabled;
/// No description provided for @appSettings_maxRouteWeight.
///
/// In en, this message translates to:
/// **'Max Route Weight'**
String get appSettings_maxRouteWeight;
/// No description provided for @appSettings_maxRouteWeightSubtitle.
///
/// In en, this message translates to:
/// **'Maximum weight a path can accumulate from successful deliveries'**
String get appSettings_maxRouteWeightSubtitle;
/// No description provided for @appSettings_initialRouteWeight.
///
/// In en, this message translates to:
/// **'Initial Route Weight'**
String get appSettings_initialRouteWeight;
/// No description provided for @appSettings_initialRouteWeightSubtitle.
///
/// In en, this message translates to:
/// **'Starting weight for newly discovered paths'**
String get appSettings_initialRouteWeightSubtitle;
/// No description provided for @appSettings_routeWeightSuccessIncrement.
///
/// In en, this message translates to:
/// **'Success Weight Increment'**
String get appSettings_routeWeightSuccessIncrement;
/// No description provided for @appSettings_routeWeightSuccessIncrementSubtitle.
///
/// In en, this message translates to:
/// **'Weight added to a path after successful delivery'**
String get appSettings_routeWeightSuccessIncrementSubtitle;
/// No description provided for @appSettings_routeWeightFailureDecrement.
///
/// In en, this message translates to:
/// **'Failure Weight Decrement'**
String get appSettings_routeWeightFailureDecrement;
/// No description provided for @appSettings_routeWeightFailureDecrementSubtitle.
///
/// In en, this message translates to:
/// **'Weight removed from a path after failed delivery'**
String get appSettings_routeWeightFailureDecrementSubtitle;
/// No description provided for @appSettings_maxMessageRetries.
///
/// In en, this message translates to:
/// **'Max Message Retries'**
String get appSettings_maxMessageRetries;
/// No description provided for @appSettings_maxMessageRetriesSubtitle.
///
/// In en, this message translates to:
/// **'Number of retry attempts before marking a message as failed'**
String get appSettings_maxMessageRetriesSubtitle;
/// No description provided for @path_routeWeight.
///
/// In en, this message translates to:
/// **'{weight}/{max}'**
String path_routeWeight(String weight, String max);
/// No description provided for @appSettings_battery.
///
/// In en, this message translates to:
@@ -1462,6 +1864,12 @@ abstract class AppLocalizations {
/// **'Group name is required'**
String get contacts_groupNameRequired;
/// No description provided for @contacts_groupNameReserved.
///
/// In en, this message translates to:
/// **'This group name is reserved'**
String get contacts_groupNameReserved;
/// No description provided for @contacts_groupAlreadyExists.
///
/// In en, this message translates to:
@@ -1489,39 +1897,105 @@ abstract class AppLocalizations {
/// No description provided for @contacts_lastSeenNow.
///
/// In en, this message translates to:
/// **'Last seen now'**
/// **'recently'**
String get contacts_lastSeenNow;
/// No description provided for @contacts_lastSeenMinsAgo.
///
/// In en, this message translates to:
/// **'Last seen {minutes} mins ago'**
/// **'~ {minutes} min.'**
String contacts_lastSeenMinsAgo(int minutes);
/// No description provided for @contacts_lastSeenHourAgo.
///
/// In en, this message translates to:
/// **'Last seen 1 hour ago'**
/// **'~ 1 hour'**
String get contacts_lastSeenHourAgo;
/// No description provided for @contacts_lastSeenHoursAgo.
///
/// In en, this message translates to:
/// **'Last seen {hours} hours ago'**
/// **'~ {hours} hours'**
String contacts_lastSeenHoursAgo(int hours);
/// No description provided for @contacts_lastSeenDayAgo.
///
/// In en, this message translates to:
/// **'Last seen 1 day ago'**
/// **'~ 1 day'**
String get contacts_lastSeenDayAgo;
/// No description provided for @contacts_lastSeenDaysAgo.
///
/// In en, this message translates to:
/// **'Last seen {days} days ago'**
/// **'~ {days} days'**
String contacts_lastSeenDaysAgo(int days);
/// No description provided for @contact_info.
///
/// In en, this message translates to:
/// **'Contact Info'**
String get contact_info;
/// No description provided for @contact_settings.
///
/// In en, this message translates to:
/// **'Contact Settings'**
String get contact_settings;
/// No description provided for @contact_telemetry.
///
/// In en, this message translates to:
/// **'Telemetry'**
String get contact_telemetry;
/// No description provided for @contact_lastSeen.
///
/// In en, this message translates to:
/// **'Last seen'**
String get contact_lastSeen;
/// No description provided for @contact_clearChat.
///
/// In en, this message translates to:
/// **'Clear Chat'**
String get contact_clearChat;
/// No description provided for @contact_teleBase.
///
/// In en, this message translates to:
/// **'Telemetry Base'**
String get contact_teleBase;
/// No description provided for @contact_teleBaseSubtitle.
///
/// In en, this message translates to:
/// **'Allow sharing battery level and basic telemetry'**
String get contact_teleBaseSubtitle;
/// No description provided for @contact_teleLoc.
///
/// In en, this message translates to:
/// **'Telemetry Location'**
String get contact_teleLoc;
/// No description provided for @contact_teleLocSubtitle.
///
/// In en, this message translates to:
/// **'Allow sharing location data'**
String get contact_teleLocSubtitle;
/// No description provided for @contact_teleEnv.
///
/// In en, this message translates to:
/// **'Telemetry Environment'**
String get contact_teleEnv;
/// No description provided for @contact_teleEnvSubtitle.
///
/// In en, this message translates to:
/// **'Allow sharing environment sensor data'**
String get contact_teleEnvSubtitle;
/// No description provided for @channels_title.
///
/// In en, this message translates to:
@@ -2494,6 +2968,12 @@ abstract class AppLocalizations {
/// **'Share marker here'**
String get map_shareMarkerHere;
/// No description provided for @map_setAsMyLocation.
///
/// In en, this message translates to:
/// **'Set as my location'**
String get map_setAsMyLocation;
/// No description provided for @map_pinLabel.
///
/// In en, this message translates to:
@@ -2578,6 +3058,12 @@ abstract class AppLocalizations {
/// **'Other Nodes'**
String get map_otherNodes;
/// No description provided for @map_showOverlaps.
///
/// In en, this message translates to:
/// **'Repeater Key Overlaps'**
String get map_showOverlaps;
/// No description provided for @map_keyPrefix.
///
/// In en, this message translates to:
@@ -2608,6 +3094,24 @@ abstract class AppLocalizations {
/// **'Show shared markers'**
String get map_showSharedMarkers;
/// No description provided for @map_showGuessedLocations.
///
/// In en, this message translates to:
/// **'Show guessed node locations'**
String get map_showGuessedLocations;
/// No description provided for @map_showDiscoveryContacts.
///
/// In en, this message translates to:
/// **'Show Discovery Contacts'**
String get map_showDiscoveryContacts;
/// No description provided for @map_guessedLocation.
///
/// In en, this message translates to:
/// **'Guessed location'**
String get map_guessedLocation;
/// No description provided for @map_lastSeenTime.
///
/// In en, this message translates to:
@@ -2641,9 +3145,15 @@ abstract class AppLocalizations {
/// No description provided for @map_runTrace.
///
/// In en, this message translates to:
/// **'Run Path Trace'**
/// **'Run path trace'**
String get map_runTrace;
/// No description provided for @map_runTraceWithReturnPath.
///
/// In en, this message translates to:
/// **'Return back on the same path.'**
String get map_runTraceWithReturnPath;
/// No description provided for @map_removeLast.
///
/// In en, this message translates to:
@@ -3812,6 +4322,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:
@@ -5380,6 +5902,288 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Last seen'**
String get snrIndicator_lastSeen;
/// No description provided for @contactsSettings_title.
///
/// In en, this message translates to:
/// **'Contacts settings'**
String get contactsSettings_title;
/// No description provided for @contactsSettings_autoAddTitle.
///
/// In en, this message translates to:
/// **'Automatic Discovery'**
String get contactsSettings_autoAddTitle;
/// No description provided for @contactsSettings_otherTitle.
///
/// In en, this message translates to:
/// **'Other contact related settings'**
String get contactsSettings_otherTitle;
/// No description provided for @contactsSettings_autoAddUsersTitle.
///
/// In en, this message translates to:
/// **'Auto-add users'**
String get contactsSettings_autoAddUsersTitle;
/// No description provided for @contactsSettings_autoAddUsersSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered users.'**
String get contactsSettings_autoAddUsersSubtitle;
/// No description provided for @contactsSettings_autoAddRepeatersTitle.
///
/// In en, this message translates to:
/// **'Auto-add repeaters'**
String get contactsSettings_autoAddRepeatersTitle;
/// No description provided for @contactsSettings_autoAddRepeatersSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered repeaters.'**
String get contactsSettings_autoAddRepeatersSubtitle;
/// No description provided for @contactsSettings_autoAddRoomServersTitle.
///
/// In en, this message translates to:
/// **'Auto-add room servers'**
String get contactsSettings_autoAddRoomServersTitle;
/// No description provided for @contactsSettings_autoAddRoomServersSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered room servers.'**
String get contactsSettings_autoAddRoomServersSubtitle;
/// No description provided for @contactsSettings_autoAddSensorsTitle.
///
/// In en, this message translates to:
/// **'Auto-add sensors'**
String get contactsSettings_autoAddSensorsTitle;
/// No description provided for @contactsSettings_autoAddSensorsSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered sensors.'**
String get contactsSettings_autoAddSensorsSubtitle;
/// No description provided for @contactsSettings_overwriteOldestTitle.
///
/// In en, this message translates to:
/// **'Overwrite Oldest'**
String get contactsSettings_overwriteOldestTitle;
/// No description provided for @contactsSettings_overwriteOldestSubtitle.
///
/// In en, this message translates to:
/// **'When the contact list is full, the oldest non-favorited contact will be replaced.'**
String get contactsSettings_overwriteOldestSubtitle;
/// No description provided for @discoveredContacts_Title.
///
/// In en, this message translates to:
/// **'Discovered Contacts'**
String get discoveredContacts_Title;
/// No description provided for @discoveredContacts_noMatching.
///
/// In en, this message translates to:
/// **'No matching contacts'**
String get discoveredContacts_noMatching;
/// No description provided for @discoveredContacts_searchHint.
///
/// In en, this message translates to:
/// **'Search discovered contacts'**
String get discoveredContacts_searchHint;
/// No description provided for @discoveredContacts_contactAdded.
///
/// In en, this message translates to:
/// **'Contact added'**
String get discoveredContacts_contactAdded;
/// No description provided for @discoveredContacts_addContact.
///
/// In en, this message translates to:
/// **'Add Contact'**
String get discoveredContacts_addContact;
/// No description provided for @discoveredContacts_copyContact.
///
/// In en, this message translates to:
/// **'Copy Contact to clipboard'**
String get discoveredContacts_copyContact;
/// No description provided for @discoveredContacts_deleteContact.
///
/// In en, this message translates to:
/// **'Delete Discovered Contact'**
String get discoveredContacts_deleteContact;
/// No description provided for @discoveredContacts_deleteContactAll.
///
/// In en, this message translates to:
/// **'Delete All Discovered Contacts'**
String get discoveredContacts_deleteContactAll;
/// No description provided for @discoveredContacts_deleteContactAllContent.
///
/// 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 @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);
}
class _AppLocalizationsDelegate
@@ -5398,7 +6202,10 @@ class _AppLocalizationsDelegate
'en',
'es',
'fr',
'hu',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
@@ -5427,8 +6234,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':
+476
View File
@@ -38,6 +38,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_delete => 'Изтрий';
@override
String get common_deleteAll => 'Изтрий всичко';
@override
String get common_close => 'Затвори';
@@ -108,6 +111,134 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Свържете се чрез TCP';
@override
String get tcpHostLabel => 'IP адрес';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Пристанище';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Въведете крайната точка и свържете се.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Свързване към $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необходим е IP адрес.';
@override
String get tcpErrorPortInvalid => 'Портът трябва да бъде между 1 и 65535.';
@override
String get tcpErrorUnsupported =>
'Транспортът чрез TCP не се поддържа на тази платформа.';
@override
String get tcpErrorTimedOut => 'Връзката TCP изтекла.';
@override
String tcpConnectionFailed(String error) {
return 'Неуспешно е установено TCP връзката: $error';
}
@override
String get usbScreenTitle => 'Свържете се чрез USB';
@override
String get usbScreenSubtitle =>
'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.';
@override
String get usbScreenStatus => 'Изберете USB устройство';
@override
String get usbScreenNote =>
'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.';
@override
String get usbScreenEmptyState =>
'Няма открити USB устройства. Включете едно и опитайте отново.';
@override
String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.';
@override
String get usbErrorDeviceMissing =>
'Избраното USB устройство вече не е налично.';
@override
String get usbErrorInvalidPort => 'Изберете валитно USB устройство.';
@override
String get usbErrorBusy =>
'Друг мол за свързване през USB вече е в процес на изпълнение.';
@override
String get usbErrorNotConnected => 'Няма свързано USB устройство.';
@override
String get usbErrorOpenFailed =>
'Не успях да отворя избраното USB устройство.';
@override
String get usbErrorConnectFailed =>
'Не успях да се свържа с избраното USB устройство.';
@override
String get usbErrorUnsupported =>
'USB серийната комуникация не се поддържа на тази платформа.';
@override
String get usbErrorAlreadyActive => 'USB връзката вече е активирана.';
@override
String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.';
@override
String get usbErrorPortClosed => 'USB връзката не е активна.';
@override
String get usbErrorConnectTimedOut =>
'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.';
@override
String get usbFallbackDeviceName =>
'Устройство за четене на уеб серийни данни';
@override
String get usbStatus_notConnected => 'Изберете USB устройство';
@override
String get usbStatus_connecting => 'Свързване към USB устройство...';
@override
String get usbStatus_searching => 'Търсене на USB устройства...';
@override
String usbConnectionFailed(String error) {
return 'Неуспешно свързване през USB: $error';
}
@override
String get scanner_scanning => 'Сканиране за устройства...';
@@ -150,6 +281,13 @@ class AppLocalizationsBg extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
@override
String get scanner_chromeRequired => 'Изисква се браузър Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.';
@override
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
@@ -234,6 +372,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_longitude => 'Дължина';
@override
String get settings_contactSettings => 'Настройки за контакти';
@override
String get settings_contactSettingsSubtitle =>
'Настройки за добавяне на контакти.';
@override
String get settings_privacyMode => 'Режим на поверителност';
@@ -253,6 +398,52 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Режим на поверителност е деактивиран';
@override
String get settings_privacy => 'Настройки на поверителността';
@override
String get settings_privacySubtitle =>
'Контролирайте каква информация се споделя.';
@override
String get settings_privacySettingsDescription =>
'Изберете каква информация устройството ви споделя с другите.';
@override
String get settings_denyAll => 'Откажи всичко';
@override
String get settings_allowByContact => 'Позволи по флагове за контакт';
@override
String get settings_allowAll => 'Позволи всичко';
@override
String get settings_telemetryBaseMode => 'Базов режим на телеметрия';
@override
String get settings_telemetryLocationMode =>
'Режим на местоположение на телеметрията';
@override
String get settings_telemetryEnvironmentMode =>
'Режим на средата на телеметрията';
@override
String get settings_advertLocation => 'Място на обявата';
@override
String get settings_advertLocationSubtitle =>
'Включи местоположение в обявата';
@override
String settings_multiAck(String value) {
return 'Мулти-потвърди: $value';
}
@override
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
@override
String get settings_actions => 'Действия';
@@ -550,6 +741,51 @@ class AppLocalizationsBg extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Автоматично маршрутизирането е деактивирано';
@override
String get appSettings_maxRouteWeight =>
'Максимално допустимо тегло на маршрута';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Максималното тегло, което един маршрут може да събере от успешни доставки.';
@override
String get appSettings_initialRouteWeight =>
'Първоначална тежест на маршрута';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Начално тегло за новооткрити маршрути';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Увеличение на теглото за успех';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Тегло, добавено към път след успешно доставяне.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Намаляване на теглото, свързано с неуспех';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Тегло, което е било премахнато от пътя след неуспешен опит за доставка.';
@override
String get appSettings_maxMessageRetries =>
'Максимален брой опити за изпращане на съобщение';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батерия';
@@ -757,6 +993,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Името на групата е задължително.';
@override
String get contacts_groupNameReserved => 'Това име на група е запазено';
@override
String contacts_groupAlreadyExists(String name) {
return 'Групата \"$name\" вече съществува.';
@@ -796,6 +1035,42 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Последно видян $days дни преди.';
}
@override
String get contact_info => 'Контактна информация';
@override
String get contact_settings => 'Настройки за контакти';
@override
String get contact_telemetry => 'Телеметрия';
@override
String get contact_lastSeen => 'Последно видян';
@override
String get contact_clearChat => 'Изчисти чата';
@override
String get contact_teleBase => 'Базата данни за телеметрия';
@override
String get contact_teleBaseSubtitle =>
'Позволи споделяне на ниво на батерията и основна телеметрия';
@override
String get contact_teleLoc => 'Местоположение на телеметрията';
@override
String get contact_teleLocSubtitle =>
'Позволи споделяне на данни за местоположение';
@override
String get contact_teleEnv => 'Среда на телеметрия';
@override
String get contact_teleEnvSubtitle =>
'Позволи споделяне на данни от средносферните датчици';
@override
String get channels_title => 'Канали';
@@ -1366,6 +1641,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Споделете маркер тук';
@override
String get map_setAsMyLocation => 'Задайте като моя местоположение';
@override
String get map_pinLabel => 'Етикетиране на пин';
@@ -1411,6 +1689,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_otherNodes => 'Други възли';
@override
String get map_showOverlaps => 'Покриване на ключа на повтаряча';
@override
String get map_keyPrefix => 'Префикс на ключа';
@@ -1426,6 +1707,16 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Покажи споделени маркери';
@override
String get map_showGuessedLocations =>
'Покажете местоположенията на предположените възли.';
@override
String get map_showDiscoveryContacts => 'Покажи контакти за откриване';
@override
String get map_guessedLocation => 'Предполагано местоположение';
@override
String get map_lastSeenTime => 'Последна видяна дата';
@@ -1445,6 +1736,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_runTrace => 'Изпълни Път на Следване';
@override
String get map_runTraceWithReturnPath => 'Върни се по същия път.';
@override
String get map_removeLast => 'Премахни Последно';
@@ -2135,6 +2429,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 => 'Изпраща рекламен пакет';
@@ -3112,4 +3412,180 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Последно видян';
@override
String get contactsSettings_title => 'Настройки на контактите';
@override
String get contactsSettings_autoAddTitle => 'Автоматично откриване';
@override
String get contactsSettings_otherTitle =>
'Други настройки свързани с контакти';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматично добавяне на потребители';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Позволи на спътника да добавя автоматично откритите потребители.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматично добавяне на повтарящи се елементи';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматично добавяне на сървъри на стаите';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Позволи на спътника да добавя автоматично откритите сървъри на стаите.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматично добавяне на датчици';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Позволи на спътника да добавя автоматично откритите датчици.';
@override
String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.';
@override
String get discoveredContacts_Title => 'Открити контакти';
@override
String get discoveredContacts_noMatching => 'Няма съвпадащи контакти';
@override
String get discoveredContacts_searchHint => 'Търсене на открити контакти';
@override
String get discoveredContacts_contactAdded => 'Контакт добавен';
@override
String get discoveredContacts_addContact => 'Добави контакт';
@override
String get discoveredContacts_copyContact => 'Копирай контакт в клипборда';
@override
String get discoveredContacts_deleteContact => 'Изтрий контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Изтриване на Всички Открити Контакти';
@override
String get discoveredContacts_deleteContactAllContent =>
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
@override
String get chat_sendCooldown =>
'Моля, изчакайте малко, преди да изпратите отново.';
@override
String get appSettings_jumpToOldestUnread =>
'Преминете към най-старата непочетена статия';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.';
@override
String get appSettings_languageHu => 'Унгарски';
@override
String get appSettings_languageJa => 'Японски';
@override
String get appSettings_languageKo => 'Корейски';
@override
String get radioStats_tooltip => 'Статистика за радио и мрежа';
@override
String get radioStats_screenTitle =>
'Статистически данни за радиопредаванията';
@override
String get radioStats_notConnected =>
'Свържете се с устройство, за да видите статистически данни за радиопредаване.';
@override
String get radioStats_firmwareTooOld =>
'Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.';
@override
String get radioStats_waiting => 'Изчакване на данни…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Ниво на шума: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Последен RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Последна стойност на SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Време на въздух (общо): $seconds секунди';
}
@override
String radioStats_rxAir(int seconds) {
return 'Общо време на използване на RX (в секунди): $seconds с';
}
@override
String get radioStats_chartCaption =>
'Ниво на шума (dBm) за последните измервания.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Ниво на шума: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Извличане на данни за радиото…';
@override
String get radioStats_settingsTile => 'Статистически данни за радиостанции';
@override
String get radioStats_settingsSubtitle =>
'Ниво на шума, RSSI, SNR и време на пренос';
@override
String get scanner_linuxPairingShowPin => 'Покажи PIN';
@override
String get scanner_linuxPairingHidePin => 'Скрий ПИН';
@override
String get scanner_linuxPairingPinTitle =>
'PIN код за сдвояване на Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Въведете ПИН за $deviceName (оставете празно, ако няма).';
}
}
+481 -6
View File
@@ -38,6 +38,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get common_delete => 'Löschen';
@override
String get common_deleteAll => 'Alles löschen';
@override
String get common_close => 'Schließen';
@@ -108,6 +111,137 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Verbinden über TCP';
@override
String get tcpHostLabel => 'IP-Adresse';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected =>
'Geben Sie den Endpunkt ein und verbinden Sie sich.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Verbindung zu $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Eine IP-Adresse ist erforderlich.';
@override
String get tcpErrorPortInvalid =>
'Die Portnummer muss zwischen 1 und 65535 liegen.';
@override
String get tcpErrorUnsupported =>
'Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.';
@override
String get tcpErrorTimedOut => 'Die TCP-Verbindung ist abgelaufen.';
@override
String tcpConnectionFailed(String error) {
return 'Fehler beim TCP-Verbindungsaufbau: $error';
}
@override
String get usbScreenTitle => 'Verbinden über USB';
@override
String get usbScreenSubtitle =>
'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.';
@override
String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus';
@override
String get usbScreenNote =>
'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.';
@override
String get usbScreenEmptyState =>
'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
@override
String get usbErrorPermissionDenied =>
'Die USB-Berechtigung wurde abgelehnt.';
@override
String get usbErrorDeviceMissing =>
'Das ausgewählte USB-Gerät ist nicht mehr verfügbar.';
@override
String get usbErrorInvalidPort => 'Wählen Sie ein gültiges USB-Gerät aus.';
@override
String get usbErrorBusy =>
'Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.';
@override
String get usbErrorNotConnected => 'Es ist kein USB-Gerät angeschlossen.';
@override
String get usbErrorOpenFailed =>
'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.';
@override
String get usbErrorConnectFailed =>
'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.';
@override
String get usbErrorUnsupported =>
'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.';
@override
String get usbErrorAlreadyActive =>
'Eine USB-Verbindung ist bereits hergestellt.';
@override
String get usbErrorNoDeviceSelected => 'Kein USB-Gerät wurde ausgewählt.';
@override
String get usbErrorPortClosed => 'Die USB-Verbindung ist nicht aktiv.';
@override
String get usbErrorConnectTimedOut =>
'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.';
@override
String get usbFallbackDeviceName => 'Web-Serielle Geräte';
@override
String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus';
@override
String get usbStatus_connecting => 'Verbindung zum USB-Gerät...';
@override
String get usbStatus_searching => 'Suche nach USB-Geräten...';
@override
String usbConnectionFailed(String error) {
return 'Fehler beim USB-Verbindungsaufbau: $error';
}
@override
String get scanner_scanning => 'Scannen nach Geräten...';
@@ -150,6 +284,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
@override
String get scanner_chromeRequired => 'Chrome Browser erforderlich';
@override
String get scanner_chromeRequiredMessage =>
'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.';
@override
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
@@ -233,6 +374,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_longitude => 'Längengrad';
@override
String get settings_contactSettings => 'Kontakteinstellungen';
@override
String get settings_contactSettingsSubtitle =>
'Einstellungen für das Hinzufügen von Kontakten';
@override
String get settings_privacyMode => 'Privatsphäreeinstellung';
@@ -250,6 +398,50 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
@override
String get settings_privacy => 'Datenschutzeinstellungen';
@override
String get settings_privacySubtitle =>
'Steuern Sie die Informationen, die freigegeben werden.';
@override
String get settings_privacySettingsDescription =>
'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.';
@override
String get settings_denyAll => 'Alle ablehnen';
@override
String get settings_allowByContact => 'Zulassen durch Kontaktflaggen';
@override
String get settings_allowAll => 'Alles zulassen';
@override
String get settings_telemetryBaseMode => 'Telemetrie-Basismodus';
@override
String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus';
@override
String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus';
@override
String get settings_advertLocation => 'Anzeigenort';
@override
String get settings_advertLocationSubtitle =>
'Ort in der Anzeige einbeziehen';
@override
String settings_multiAck(String value) {
return 'Mehrfach-Bestätigungen: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
@override
String get settings_actions => 'Aktionen';
@@ -547,6 +739,49 @@ class AppLocalizationsDe extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatische Routenrotation deaktiviert';
@override
String get appSettings_maxRouteWeight => 'Maximale Gesamtstreckenlänge';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.';
@override
String get appSettings_initialRouteWeight => 'Anfangs-Streckengewicht';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Ausgangsgewicht für neu entdeckte Pfade';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Erhöhung des Erfolgsgewichts';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Reduzierung des Gewichts bei Fehlern';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde';
@override
String get appSettings_maxMessageRetries =>
'Maximale Anzahl an Wiederholungsversuchen';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Akku';
@@ -754,6 +989,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
@override
String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert';
@override
String contacts_groupAlreadyExists(String name) {
return 'Die Gruppe \"$name\" existiert bereits.';
@@ -770,29 +1008,64 @@ class AppLocalizationsDe extends AppLocalizations {
String get contacts_noMembers => 'Keine Mitglieder';
@override
String get contacts_lastSeenNow => 'gerade gesehen';
String get contacts_lastSeenNow => 'kürzlich';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Letzte Sichtung vor $minutes Minuten.';
return '~ $minutes Min.';
}
@override
String get contacts_lastSeenHourAgo => 'Letzte Sichtung vor 1 Stunde.';
String get contacts_lastSeenHourAgo => '~ 1 Std.';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Letzte Sichtung vor $hours Stunden.';
return '~ $hours Std.';
}
@override
String get contacts_lastSeenDayAgo => 'Letzte Sichtung vor 1 Tag';
String get contacts_lastSeenDayAgo => '~ 1 Tag';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Letzte Sichtung $days Tage zuvor';
return '~ $days Tage';
}
@override
String get contact_info => 'Kontaktinformationen';
@override
String get contact_settings => 'Kontakteinstellungen';
@override
String get contact_telemetry => 'Telemetrie';
@override
String get contact_lastSeen => 'Zuletzt gesehen';
@override
String get contact_clearChat => 'Chat löschen';
@override
String get contact_teleBase => 'Telemetriebasis';
@override
String get contact_teleBaseSubtitle =>
'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie';
@override
String get contact_teleLoc => 'Telemetrieort';
@override
String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen';
@override
String get contact_teleEnv => 'Telemetrieumgebung';
@override
String get contact_teleEnvSubtitle =>
'Teilen von Umgebungsensordaten zulassen';
@override
String get channels_title => 'Kanäle';
@@ -1365,6 +1638,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
@override
String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen';
@override
String get map_pinLabel => 'Pin Name';
@@ -1410,6 +1686,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Knoten';
@override
String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
@override
String get map_keyPrefix => 'Schlüsselpräfix';
@@ -1425,6 +1704,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker';
@override
String get map_showGuessedLocations =>
'Zeige die vermuteten Knotenpositionen';
@override
String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen';
@override
String get map_guessedLocation => 'Geschätzter Ort';
@override
String get map_lastSeenTime => 'Letzte Sichtung';
@@ -1444,6 +1733,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_runTrace => 'Pfadverlauf ausführen';
@override
String get map_runTraceWithReturnPath =>
'Auf dem gleichen Pfad zurückkehren.';
@override
String get map_removeLast => 'Letztes Entfernen';
@@ -2136,6 +2429,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';
@@ -3121,4 +3420,180 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
@override
String get contactsSettings_title => 'Kontakteinstellungen';
@override
String get contactsSettings_autoAddTitle => 'Automatische Erkennung';
@override
String get contactsSettings_otherTitle =>
'Weitere Einstellungen zu Kontakten';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automatische Hinzufügung von Benutzern';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automatisch Repeater hinzufügen';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatisch Raumservers hinzufügen';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automatisch Sensoren hinzufügen';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen';
@override
String get contactsSettings_overwriteOldestTitle =>
'Überschreiben des Ältesten';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.';
@override
String get discoveredContacts_Title => 'Entdeckte Kontakte';
@override
String get discoveredContacts_noMatching => 'Keine passenden Kontakte';
@override
String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen';
@override
String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt';
@override
String get discoveredContacts_addContact => 'Kontakt hinzufügen';
@override
String get discoveredContacts_copyContact =>
'Kontakt in die Zwischenablage kopieren';
@override
String get discoveredContacts_deleteContact => 'Kontakt löschen';
@override
String get discoveredContacts_deleteContactAll =>
'Alle entdeckten Kontakte löschen';
@override
String get discoveredContacts_deleteContactAllContent =>
'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?';
@override
String get chat_sendCooldown =>
'Bitte warten Sie einen Moment, bevor Sie erneut senden.';
@override
String get appSettings_jumpToOldestUnread =>
'Zum ältesten, nicht gelesenen Eintrag springen';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.';
@override
String get appSettings_languageHu => 'Ungarisch';
@override
String get appSettings_languageJa => 'Japanisch';
@override
String get appSettings_languageKo => 'Koreanisch';
@override
String get radioStats_tooltip => 'Daten zu Radio- und Mesh-Netzwerken';
@override
String get radioStats_screenTitle => 'Senderinformationen';
@override
String get radioStats_notConnected =>
'Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.';
@override
String get radioStats_firmwareTooOld =>
'Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.';
@override
String get radioStats_waiting => 'Warte auf Daten…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Rauschpegel: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Letzter RSSI-Wert: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Letzter SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Gesamt-TX-Zeit: $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Gesamt-RX-Zeit: $seconds s';
}
@override
String get radioStats_chartCaption =>
'Rauschpegel (dBm) basierend auf den letzten Messwerten.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Rauschpegel: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Abrufen von Radiostatus…';
@override
String get radioStats_settingsTile => 'Senderinformationen';
@override
String get radioStats_settingsSubtitle =>
'Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit';
@override
String get 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).';
}
}
+463 -7
View File
@@ -38,6 +38,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_delete => 'Delete';
@override
String get common_deleteAll => 'Delete All';
@override
String get common_close => 'Close';
@@ -108,6 +111,132 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Connect over TCP';
@override
String get tcpHostLabel => 'IP Address';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Enter endpoint and connect';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connecting to $endpoint...';
}
@override
String get tcpErrorHostRequired => 'IP address is required.';
@override
String get tcpErrorPortInvalid => 'Port must be between 1 and 65535.';
@override
String get tcpErrorUnsupported =>
'TCP transport is not supported on this platform.';
@override
String get tcpErrorTimedOut => 'TCP connection timed out.';
@override
String tcpConnectionFailed(String error) {
return 'TCP connection failed: $error';
}
@override
String get usbScreenTitle => 'Connect over USB';
@override
String get usbScreenSubtitle =>
'Choose a detected serial device and connect directly to your MeshCore node.';
@override
String get usbScreenStatus => 'Select a USB device';
@override
String get usbScreenNote =>
'USB serial is active on supported Android devices and desktop platforms.';
@override
String get usbScreenEmptyState =>
'No USB devices found. Plug one in and refresh.';
@override
String get usbErrorPermissionDenied => 'USB permission was denied.';
@override
String get usbErrorDeviceMissing =>
'The selected USB device is no longer available.';
@override
String get usbErrorInvalidPort => 'Select a valid USB device.';
@override
String get usbErrorBusy =>
'Another USB connection request is already in progress.';
@override
String get usbErrorNotConnected => 'No USB device is connected.';
@override
String get usbErrorOpenFailed => 'Failed to open the selected USB device.';
@override
String get usbErrorConnectFailed =>
'Failed to connect to the selected USB device.';
@override
String get usbErrorUnsupported =>
'USB serial is not supported on this platform.';
@override
String get usbErrorAlreadyActive => 'A USB connection is already active.';
@override
String get usbErrorNoDeviceSelected => 'No USB device was selected.';
@override
String get usbErrorPortClosed => 'The USB connection is not open.';
@override
String get usbErrorConnectTimedOut =>
'Connection timed out. Make sure the device has USB Companion firmware.';
@override
String get usbFallbackDeviceName => 'Web Serial Device';
@override
String get usbStatus_notConnected => 'Select a USB device';
@override
String get usbStatus_connecting => 'Connecting to USB device...';
@override
String get usbStatus_searching => 'Searching for USB devices...';
@override
String usbConnectionFailed(String error) {
return 'USB connection failed: $error';
}
@override
String get scanner_scanning => 'Scanning for devices...';
@@ -149,6 +278,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Please turn on Bluetooth to scan for devices';
@override
String get scanner_chromeRequired => 'Chrome Browser Required';
@override
String get scanner_chromeRequiredMessage =>
'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.';
@override
String get scanner_enableBluetooth => 'Enable Bluetooth';
@@ -232,6 +368,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Contact Settings';
@override
String get settings_contactSettingsSubtitle =>
'Settings for how contacts are added.';
@override
String get settings_privacyMode => 'Privacy Mode';
@@ -249,6 +392,48 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privacy mode disabled';
@override
String get settings_privacy => 'Privacy Settings';
@override
String get settings_privacySubtitle => 'Control what information is shared.';
@override
String get settings_privacySettingsDescription =>
'Choose what information your device shares with others.';
@override
String get settings_denyAll => 'Deny all';
@override
String get settings_allowByContact => 'Allow by contact flags';
@override
String get settings_allowAll => 'Allow all';
@override
String get settings_telemetryBaseMode => 'Telemetry Base Mode';
@override
String get settings_telemetryLocationMode => 'Telemetry Location Mode';
@override
String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode';
@override
String get settings_advertLocation => 'Advert Location';
@override
String get settings_advertLocationSubtitle => 'Include location in advert.';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
@override
String get settings_actions => 'Actions';
@@ -541,6 +726,48 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Auto route rotation disabled';
@override
String get appSettings_maxRouteWeight => 'Max Route Weight';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximum weight a path can accumulate from successful deliveries';
@override
String get appSettings_initialRouteWeight => 'Initial Route Weight';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Starting weight for newly discovered paths';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Success Weight Increment';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Weight added to a path after successful delivery';
@override
String get appSettings_routeWeightFailureDecrement =>
'Failure Weight Decrement';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Weight removed from a path after failed delivery';
@override
String get appSettings_maxMessageRetries => 'Max Message Retries';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Number of retry attempts before marking a message as failed';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Battery';
@@ -746,6 +973,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Group name is required';
@override
String get contacts_groupNameReserved => 'This group name is reserved';
@override
String contacts_groupAlreadyExists(String name) {
return 'Group \"$name\" already exists';
@@ -761,29 +991,63 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_noMembers => 'No members';
@override
String get contacts_lastSeenNow => 'Last seen now';
String get contacts_lastSeenNow => 'recently';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Last seen $minutes mins ago';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago';
String get contacts_lastSeenHourAgo => '~ 1 hour';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Last seen $hours hours ago';
return '~ $hours hours';
}
@override
String get contacts_lastSeenDayAgo => 'Last seen 1 day ago';
String get contacts_lastSeenDayAgo => '~ 1 day';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Last seen $days days ago';
return '~ $days days';
}
@override
String get contact_info => 'Contact Info';
@override
String get contact_settings => 'Contact Settings';
@override
String get contact_telemetry => 'Telemetry';
@override
String get contact_lastSeen => 'Last seen';
@override
String get contact_clearChat => 'Clear Chat';
@override
String get contact_teleBase => 'Telemetry Base';
@override
String get contact_teleBaseSubtitle =>
'Allow sharing battery level and basic telemetry';
@override
String get contact_teleLoc => 'Telemetry Location';
@override
String get contact_teleLocSubtitle => 'Allow sharing location data';
@override
String get contact_teleEnv => 'Telemetry Environment';
@override
String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data';
@override
String get channels_title => 'Channels';
@@ -1344,6 +1608,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Share marker here';
@override
String get map_setAsMyLocation => 'Set as my location';
@override
String get map_pinLabel => 'Pin label';
@@ -1389,6 +1656,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_otherNodes => 'Other Nodes';
@override
String get map_showOverlaps => 'Repeater Key Overlaps';
@override
String get map_keyPrefix => 'Key Prefix';
@@ -1404,6 +1674,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Show shared markers';
@override
String get map_showGuessedLocations => 'Show guessed node locations';
@override
String get map_showDiscoveryContacts => 'Show Discovery Contacts';
@override
String get map_guessedLocation => 'Guessed location';
@override
String get map_lastSeenTime => 'Last Seen Time';
@@ -1420,7 +1699,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
@override
String get map_runTrace => 'Run Path Trace';
String get map_runTrace => 'Run path trace';
@override
String get map_runTraceWithReturnPath => 'Return back on the same path.';
@override
String get map_removeLast => 'Remove Last';
@@ -2097,6 +2379,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';
@@ -3065,4 +3353,172 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Last seen';
@override
String get contactsSettings_title => 'Contacts settings';
@override
String get contactsSettings_autoAddTitle => 'Automatic Discovery';
@override
String get contactsSettings_otherTitle => 'Other contact related settings';
@override
String get contactsSettings_autoAddUsersTitle => 'Auto-add users';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Allow the companion to automatically add discovered users.';
@override
String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Allow the companion to automatically add discovered repeaters.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Auto-add room servers';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Allow the companion to automatically add discovered room servers.';
@override
String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Allow the companion to automatically add discovered sensors.';
@override
String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'When the contact list is full, the oldest non-favorited contact will be replaced.';
@override
String get discoveredContacts_Title => 'Discovered Contacts';
@override
String get discoveredContacts_noMatching => 'No matching contacts';
@override
String get discoveredContacts_searchHint => 'Search discovered contacts';
@override
String get discoveredContacts_contactAdded => 'Contact added';
@override
String get discoveredContacts_addContact => 'Add Contact';
@override
String get discoveredContacts_copyContact => 'Copy Contact to clipboard';
@override
String get discoveredContacts_deleteContact => 'Delete Discovered Contact';
@override
String get discoveredContacts_deleteContactAll =>
'Delete All Discovered Contacts';
@override
String get discoveredContacts_deleteContactAllContent =>
'Are you sure you want to delete all discovered contacts?';
@override
String get chat_sendCooldown => 'Please wait a moment before sending again.';
@override
String get appSettings_jumpToOldestUnread => 'Jump to oldest unread';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'When opening a chat with unread messages, scroll to the first unread instead of the latest.';
@override
String get appSettings_languageHu => 'Hungarian';
@override
String get appSettings_languageJa => 'Japanese';
@override
String get appSettings_languageKo => 'Korean';
@override
String get radioStats_tooltip => 'Radio & mesh stats';
@override
String get radioStats_screenTitle => 'Radio stats';
@override
String get radioStats_notConnected =>
'Connect to a device to view radio statistics.';
@override
String get radioStats_firmwareTooOld =>
'Radio statistics require companion firmware v8 or newer.';
@override
String get radioStats_waiting => 'Waiting for data…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Noise floor: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Last RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Last SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX airtime (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX airtime (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Noise floor (dBm) over recent samples.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Noise floor: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Fetching radio stats…';
@override
String get radioStats_settingsTile => 'Radio stats';
@override
String get radioStats_settingsSubtitle =>
'Noise floor, RSSI, SNR, and airtime';
@override
String get 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).';
}
}
+481 -5
View File
@@ -38,6 +38,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_delete => 'Eliminar';
@override
String get common_deleteAll => 'Eliminar todo';
@override
String get common_close => 'Cerrar';
@@ -108,6 +111,135 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Establecer conexión a través de TCP';
@override
String get tcpHostLabel => 'Dirección IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Puerto';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Ingrese la dirección final y conecte.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Conectándose a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Se requiere la dirección IP.';
@override
String get tcpErrorPortInvalid => 'El puerto debe estar entre 1 y 65535.';
@override
String get tcpErrorUnsupported =>
'El protocolo de transporte TCP no está soportado en esta plataforma.';
@override
String get tcpErrorTimedOut => 'La conexión TCP ha caducado.';
@override
String tcpConnectionFailed(String error) {
return 'Error en la conexión TCP: $error';
}
@override
String get usbScreenTitle => 'Conecte mediante USB';
@override
String get usbScreenSubtitle =>
'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.';
@override
String get usbScreenStatus => 'Seleccione un dispositivo USB';
@override
String get usbScreenNote =>
'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.';
@override
String get usbScreenEmptyState =>
'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.';
@override
String get usbErrorPermissionDenied =>
'Se denegó el permiso de acceso a través de USB.';
@override
String get usbErrorDeviceMissing =>
'El dispositivo USB seleccionado ya no está disponible.';
@override
String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.';
@override
String get usbErrorBusy =>
'Ya se ha iniciado una solicitud de conexión USB adicional.';
@override
String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.';
@override
String get usbErrorOpenFailed =>
'No se pudo abrir el dispositivo USB seleccionado.';
@override
String get usbErrorConnectFailed =>
'No se pudo conectar con el dispositivo USB seleccionado.';
@override
String get usbErrorUnsupported =>
'La comunicación serial a través de USB no está soportada en esta plataforma.';
@override
String get usbErrorAlreadyActive => 'La conexión USB ya está activa.';
@override
String get usbErrorNoDeviceSelected =>
'No se ha seleccionado ningún dispositivo USB.';
@override
String get usbErrorPortClosed => 'La conexión USB no está activa.';
@override
String get usbErrorConnectTimedOut =>
'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Dispositivo de serie web';
@override
String get usbStatus_notConnected => 'Seleccione un dispositivo USB';
@override
String get usbStatus_connecting => 'Conectándose al dispositivo USB...';
@override
String get usbStatus_searching => 'Buscando dispositivos USB...';
@override
String usbConnectionFailed(String error) {
return 'Error al conectar mediante USB: $error';
}
@override
String get scanner_scanning => 'Escaneando dispositivos...';
@@ -150,6 +282,13 @@ class AppLocalizationsEs extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, active el Bluetooth para escanear dispositivos.';
@override
String get scanner_chromeRequired => 'Navegador Chrome requerido';
@override
String get scanner_chromeRequiredMessage =>
'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.';
@override
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
@@ -233,6 +372,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_longitude => 'Longitud';
@override
String get settings_contactSettings => 'Configuración de contacto';
@override
String get settings_contactSettingsSubtitle =>
'Configuración de cómo se agregan los contactos.';
@override
String get settings_privacyMode => 'Modo Privacidad';
@@ -250,6 +396,51 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
@override
String get settings_privacy => 'Configuración de privacidad';
@override
String get settings_privacySubtitle =>
'Controlar qué información se comparte.';
@override
String get settings_privacySettingsDescription =>
'Elige qué información comparte tu dispositivo con otros.';
@override
String get settings_denyAll => 'Denegar todo';
@override
String get settings_allowByContact => 'Permitir por banderas de contacto';
@override
String get settings_allowAll => 'Permitir todo';
@override
String get settings_telemetryBaseMode => 'Modo base de telemetría';
@override
String get settings_telemetryLocationMode =>
'Modo de ubicación de telemetría';
@override
String get settings_telemetryEnvironmentMode =>
'Modo de entorno de telemetría';
@override
String get settings_advertLocation => 'Ubicación de anuncio';
@override
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
@override
String get settings_actions => 'Acciones';
@@ -548,6 +739,49 @@ class AppLocalizationsEs extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotación de ruta automática desactivada';
@override
String get appSettings_maxRouteWeight => 'Peso máximo permitido para la ruta';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Peso máximo que una ruta puede acumular gracias a entregas exitosas.';
@override
String get appSettings_initialRouteWeight => 'Peso inicial de la ruta';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Peso inicial para rutas recién descubiertas';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Incremento de peso para el éxito';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Peso añadido a una ruta después de una entrega exitosa.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Reducción del peso asociado al fallo';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Peso retirado de un camino después de un intento de entrega fallido.';
@override
String get appSettings_maxMessageRetries =>
'Número máximo de reintentos de envío de mensajes';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Número de intentos de reintento antes de marcar un mensaje como fallido.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batería';
@@ -755,6 +989,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
@override
String get contacts_groupNameReserved =>
'Este nombre de grupo está reservado';
@override
String contacts_groupAlreadyExists(String name) {
return 'El grupo \"$name\" ya existe';
@@ -775,25 +1013,61 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Última vez visto hace $minutes minutos.';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora';
String get contacts_lastSeenHourAgo => '~ 1 hora';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Última vez visto hace $hours horas.';
return '~ $hours horas';
}
@override
String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día';
String get contacts_lastSeenDayAgo => '~ 1 día';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Última vez visto hace $days días.';
return '~ $days días';
}
@override
String get contact_info => 'Información de contacto';
@override
String get contact_settings => 'Configuración de contacto';
@override
String get contact_telemetry => 'Telemetría';
@override
String get contact_lastSeen => 'Visto por última vez';
@override
String get contact_clearChat => 'Borrar chat';
@override
String get contact_teleBase => 'Base de Telemetría';
@override
String get contact_teleBaseSubtitle =>
'Permitir el intercambio de nivel de batería y telemetría básica';
@override
String get contact_teleLoc => 'Ubicación de telemetría';
@override
String get contact_teleLocSubtitle =>
'Permitir el intercambio de datos de ubicación';
@override
String get contact_teleEnv => 'Entorno de Telemetría';
@override
String get contact_teleEnvSubtitle =>
'Permitir el intercambio de datos de sensores de entorno';
@override
String get channels_title => 'Canales';
@@ -1363,6 +1637,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Compartir marcador aquí';
@override
String get map_setAsMyLocation => 'Establecer mi ubicación';
@override
String get map_pinLabel => 'Etiqueta de marcador';
@@ -1408,6 +1685,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_otherNodes => 'Otros Nodos';
@override
String get map_showOverlaps => 'Superposiciones de tecla repetidora';
@override
String get map_keyPrefix => 'Prefijo de clave';
@@ -1423,6 +1703,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
@override
String get map_showGuessedLocations =>
'Mostrar las ubicaciones estimadas de los nodos.';
@override
String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento';
@override
String get map_guessedLocation => 'Ubicación estimada';
@override
String get map_lastSeenTime => 'Última vez que se vio';
@@ -1441,6 +1731,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
@override
String get map_runTraceWithReturnPath => 'Volver atrás por el mismo camino.';
@override
String get map_removeLast => 'Eliminar último';
@@ -2130,6 +2423,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';
@@ -3113,4 +3412,181 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Visto por última vez';
@override
String get contactsSettings_title => 'Configuración de contactos';
@override
String get contactsSettings_autoAddTitle => 'Detección automática';
@override
String get contactsSettings_otherTitle =>
'Otras configuraciones relacionadas con el contacto';
@override
String get contactsSettings_autoAddUsersTitle =>
'Agregar usuarios automáticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Agregar repetidores automáticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Permitir que el compañero agregue automáticamente los repetidores descubiertos.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Agregar automáticamente servidores de sala';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Agregar sensores automáticamente';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Permitir que el compañero agregue automáticamente los sensores descubiertos.';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sobreescribir el más antiguo';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.';
@override
String get discoveredContacts_Title => 'Contactos descubiertos';
@override
String get discoveredContacts_noMatching =>
'No se encontraron contactos coincidentes';
@override
String get discoveredContacts_searchHint => 'Buscar contactos descubiertos';
@override
String get discoveredContacts_contactAdded => 'Contacto agregado';
@override
String get discoveredContacts_addContact => 'Agregar contacto';
@override
String get discoveredContacts_copyContact =>
'Copiar contacto al portapapeles';
@override
String get discoveredContacts_deleteContact => 'Eliminar contacto';
@override
String get discoveredContacts_deleteContactAll =>
'Eliminar Todos los Contactos Descubiertos';
@override
String get discoveredContacts_deleteContactAllContent =>
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
@override
String get chat_sendCooldown =>
'Por favor, espere un momento antes de reenviar.';
@override
String get appSettings_jumpToOldestUnread =>
'Salta a los mensajes más antiguos sin leer';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.';
@override
String get appSettings_languageHu => 'Húngaro';
@override
String get appSettings_languageJa => 'Japonés';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Estadísticas de radio y malla';
@override
String get radioStats_screenTitle => 'Estadísticas de radio';
@override
String get radioStats_notConnected =>
'Conéctese a un dispositivo para visualizar estadísticas de radio.';
@override
String get radioStats_firmwareTooOld =>
'Las estadísticas de radio requieren un firmware compatible v8 o posterior.';
@override
String get radioStats_waiting => 'Esperando datos…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Nivel de ruido: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Último RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Último SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tiempo de emisión en Texas (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tiempo de transmisión de RX (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Nivel de ruido (dBm) en muestras recientes.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Nivel de ruido: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Obteniendo estadísticas de la radio…';
@override
String get radioStats_settingsTile => 'Estadísticas de radio';
@override
String get radioStats_settingsSubtitle =>
'Nivel de ruido, RSSI, SNR y tiempo de transmisión';
@override
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
@override
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN de emparejamiento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Introduzca el PIN para $deviceName (déjelo en blanco si no hay ninguno).';
}
}
+485 -5
View File
@@ -38,6 +38,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_delete => 'Supprimer';
@override
String get common_deleteAll => 'Supprimer tout';
@override
String get common_close => 'Fermer';
@@ -108,6 +111,137 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Établir une connexion via TCP';
@override
String get tcpHostLabel => 'Adresse IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected =>
'Entrez l\'adresse de destination et connectez-vous.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connexion à $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Une adresse IP est obligatoire.';
@override
String get tcpErrorPortInvalid =>
'La taille du port doit être comprise entre 1 et 65535.';
@override
String get tcpErrorUnsupported =>
'Le protocole TCP n\'est pas pris en charge sur cette plateforme.';
@override
String get tcpErrorTimedOut => 'La connexion TCP a expiré.';
@override
String tcpConnectionFailed(String error) {
return 'Échec de la connexion TCP : $error';
}
@override
String get usbScreenTitle => 'Connectez via USB';
@override
String get usbScreenSubtitle =>
'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.';
@override
String get usbScreenStatus => 'Sélectionnez un périphérique USB';
@override
String get usbScreenNote =>
'La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.';
@override
String get usbScreenEmptyState =>
'Aucun périphérique USB n\'a été trouvé. Veuillez en brancher un et rafraîchir la page.';
@override
String get usbErrorPermissionDenied => 'L\'accès via USB a été refusé.';
@override
String get usbErrorDeviceMissing =>
'Le périphérique USB sélectionné n\'est plus disponible.';
@override
String get usbErrorInvalidPort => 'Sélectionnez un périphérique USB valide.';
@override
String get usbErrorBusy =>
'Une autre demande de connexion USB est déjà en cours.';
@override
String get usbErrorNotConnected => 'Aucun appareil USB n\'est connecté.';
@override
String get usbErrorOpenFailed =>
'Impossible d\'ouvrir l\'appareil USB sélectionné.';
@override
String get usbErrorConnectFailed =>
'Impossible de se connecter à l\'appareil USB sélectionné.';
@override
String get usbErrorUnsupported =>
'La communication série USB n\'est pas prise en charge sur cette plateforme.';
@override
String get usbErrorAlreadyActive => 'Une connexion USB est déjà établie.';
@override
String get usbErrorNoDeviceSelected =>
'Aucun appareil USB n\'a été sélectionné.';
@override
String get usbErrorPortClosed => 'La connexion USB n\'est pas établie.';
@override
String get usbErrorConnectTimedOut =>
'La connexion a expiré. Assurez-vous que l\'appareil dispose du firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Dispositif de communication série sur le Web';
@override
String get usbStatus_notConnected => 'Sélectionnez un périphérique USB';
@override
String get usbStatus_connecting => 'Connexion au périphérique USB...';
@override
String get usbStatus_searching => 'Recherche de périphériques USB...';
@override
String usbConnectionFailed(String error) {
return 'Échec de la connexion USB : $error';
}
@override
String get scanner_scanning => 'Recherche de périphériques...';
@@ -150,6 +284,13 @@ class AppLocalizationsFr extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Veuillez activer le Bluetooth pour rechercher des appareils.';
@override
String get scanner_chromeRequired => 'Navigateur Chrome requis';
@override
String get scanner_chromeRequiredMessage =>
'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.';
@override
String get scanner_enableBluetooth => 'Activer le Bluetooth';
@@ -234,6 +375,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Paramètres de contact';
@override
String get settings_contactSettingsSubtitle =>
'Paramètres pour l\'ajout de contacts';
@override
String get settings_privacyMode => 'Mode de confidentialité';
@@ -252,6 +400,52 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Mode de confidentialité désactivé';
@override
String get settings_privacy => 'Paramètres de confidentialité';
@override
String get settings_privacySubtitle => 'Contrôlez les informations partagées';
@override
String get settings_privacySettingsDescription =>
'Choisissez les informations que votre appareil partage avec les autres.';
@override
String get settings_denyAll => 'Refuser tout';
@override
String get settings_allowByContact => 'Autoriser par drapeaux de contact';
@override
String get settings_allowAll => 'Autoriser tout';
@override
String get settings_telemetryBaseMode => 'Mode de base Télémétrie';
@override
String get settings_telemetryLocationMode =>
'Mode d\'emplacement de télémétrie';
@override
String get settings_telemetryEnvironmentMode =>
'Mode d\'environnement de télémétrie';
@override
String get settings_advertLocation => 'Emplacement de l\'annonce';
@override
String get settings_advertLocationSubtitle =>
'Inclure l\'emplacement dans l\'annonce';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs : $value';
}
@override
String get settings_telemetryModeUpdated =>
'Le mode télémétrie a été mis à jour';
@override
String get settings_actions => 'Actions';
@@ -550,6 +744,50 @@ class AppLocalizationsFr extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotation de l\'itinéraire automatique désactivée';
@override
String get appSettings_maxRouteWeight =>
'Poids maximal autorisé pour le trajet';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Poids maximal qu\'un itinéraire peut accumuler grâce à des livraisons réussies.';
@override
String get appSettings_initialRouteWeight => 'Poids initial de l\'itinéraire';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Poids de départ pour les nouveaux chemins découverts';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Augmentation du poids de réussite';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Poids ajouté à un itinéraire après une livraison réussie.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Réduction du poids de pénalité';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Poids retiré d\'un itinéraire après une tentative de livraison infructueuse.';
@override
String get appSettings_maxMessageRetries =>
'Nombre maximal de tentatives de récupération de messages';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batterie';
@@ -757,6 +995,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
@override
String get contacts_groupNameReserved => 'Ce nom de groupe est réservé';
@override
String contacts_groupAlreadyExists(String name) {
return 'Le groupe \"$name\" existe déjà.';
@@ -777,25 +1018,61 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Vu il y a $minutes minutes';
return '~ $minutes min.';
}
@override
String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure';
String get contacts_lastSeenHourAgo => '~ 1 heure';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Vu il y a $hours heures';
return '~ $hours heures';
}
@override
String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour';
String get contacts_lastSeenDayAgo => '~ 1 jour';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Vu il y a $days jours';
return '~ $days jours';
}
@override
String get contact_info => 'Informations de contact';
@override
String get contact_settings => 'Paramètres de contact';
@override
String get contact_telemetry => 'Télémétrie';
@override
String get contact_lastSeen => 'Dernière fois vu';
@override
String get contact_clearChat => 'Effacer la conversation';
@override
String get contact_teleBase => 'Base de télémétrie';
@override
String get contact_teleBaseSubtitle =>
'Autoriser le partage du niveau de batterie et de la télémétrie de base';
@override
String get contact_teleLoc => 'Emplacement de télémétrie';
@override
String get contact_teleLocSubtitle =>
'Autoriser le partage des données de localisation';
@override
String get contact_teleEnv => 'Environnement Télémétrie';
@override
String get contact_teleEnvSubtitle =>
'Autoriser le partage des données des capteurs d\'environnement';
@override
String get channels_title => 'Canaux';
@@ -1370,6 +1647,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Partager le marqueur ici';
@override
String get map_setAsMyLocation => 'Définir comme ma localisation';
@override
String get map_pinLabel => 'Étiquete de repin';
@@ -1415,6 +1695,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_otherNodes => 'Autres nœuds';
@override
String get map_showOverlaps => 'Chevauchement de la touche répétitive';
@override
String get map_keyPrefix => 'Préfixe clé';
@@ -1430,6 +1713,16 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Afficher les marqueurs partagés';
@override
String get map_showGuessedLocations =>
'Afficher les emplacements des nœuds estimés';
@override
String get map_showDiscoveryContacts => 'Afficher les contacts de découverte';
@override
String get map_guessedLocation => 'Lieu deviné';
@override
String get map_lastSeenTime => 'Dernière fois vu';
@@ -1449,6 +1742,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_runTrace => 'Exécuter la traçage de chemin';
@override
String get map_runTraceWithReturnPath => 'Revenir sur le même chemin.';
@override
String get map_removeLast => 'Supprimer le dernier';
@@ -2146,6 +2442,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';
@@ -3135,4 +3437,182 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Dernière fois vu';
@override
String get contactsSettings_title => 'Paramètres des contacts';
@override
String get contactsSettings_autoAddTitle => 'Découverte automatique';
@override
String get contactsSettings_otherTitle =>
'Autres paramètres liés aux contacts';
@override
String get contactsSettings_autoAddUsersTitle =>
'Ajouter automatiquement les utilisateurs';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Ajouter automatiquement les répéteurs';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les répéteurs découverts';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Ajouter automatiquement les serveurs de salle';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Ajouter automatiquement les capteurs';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les capteurs découverts.';
@override
String get contactsSettings_overwriteOldestTitle => 'Écraser le plus ancien';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.';
@override
String get discoveredContacts_Title => 'Contacts découverts';
@override
String get discoveredContacts_noMatching => 'Aucun contact correspondant';
@override
String get discoveredContacts_searchHint =>
'Rechercher des contacts découverts';
@override
String get discoveredContacts_contactAdded => 'Contact ajouté';
@override
String get discoveredContacts_addContact => 'Ajouter un contact';
@override
String get discoveredContacts_copyContact =>
'Copier le contact dans le presse-papiers';
@override
String get discoveredContacts_deleteContact => 'Supprimer le contact';
@override
String get discoveredContacts_deleteContactAll =>
'Supprimer tous les contacts découverts';
@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 scanner_linuxPairingShowPin => 'Afficher le code PIN';
@override
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
@override
String get scanner_linuxPairingPinTitle => 'Code PIN dappairage Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Entrez le code PIN pour $deviceName (laissez vide si aucun).';
}
}
File diff suppressed because it is too large Load Diff
+477
View File
@@ -38,6 +38,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_delete => 'Elimina';
@override
String get common_deleteAll => 'Elimina tutto';
@override
String get common_close => 'Chiudi';
@@ -108,6 +111,137 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Stabilire una connessione tramite TCP';
@override
String get tcpHostLabel => 'Indirizzo IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Porta';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Inserisci l\'endpoint e connettiti.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Connessione a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'È necessario fornire un indirizzo IP.';
@override
String get tcpErrorPortInvalid =>
'La dimensione della porta deve essere compresa tra 1 e 65535.';
@override
String get tcpErrorUnsupported =>
'Il protocollo TCP non è supportato su questa piattaforma.';
@override
String get tcpErrorTimedOut => 'La connessione TCP è scaduta.';
@override
String tcpConnectionFailed(String error) {
return 'Impossibile stabilire la connessione TCP: $error';
}
@override
String get usbScreenTitle => 'Connessione tramite USB';
@override
String get usbScreenSubtitle =>
'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.';
@override
String get usbScreenStatus => 'Seleziona un dispositivo USB';
@override
String get usbScreenNote =>
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
@override
String get usbScreenEmptyState =>
'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.';
@override
String get usbErrorPermissionDenied =>
'È stato negato l\'accesso tramite USB.';
@override
String get usbErrorDeviceMissing =>
'Il dispositivo USB selezionato non è più disponibile.';
@override
String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.';
@override
String get usbErrorBusy =>
'Un\'altra richiesta di connessione tramite USB è già in corso.';
@override
String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.';
@override
String get usbErrorOpenFailed =>
'Impossibile aprire il dispositivo USB selezionato.';
@override
String get usbErrorConnectFailed =>
'Impossibile connettersi al dispositivo USB selezionato.';
@override
String get usbErrorUnsupported =>
'La comunicazione seriale tramite USB non è supportata su questa piattaforma.';
@override
String get usbErrorAlreadyActive => 'La connessione USB è già attiva.';
@override
String get usbErrorNoDeviceSelected =>
'Non è stato selezionato alcun dispositivo USB.';
@override
String get usbErrorPortClosed => 'La connessione USB non è attiva.';
@override
String get usbErrorConnectTimedOut =>
'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Dispositivo per comunicazione seriale su rete';
@override
String get usbStatus_notConnected => 'Seleziona un dispositivo USB';
@override
String get usbStatus_connecting => 'Connessione al dispositivo USB...';
@override
String get usbStatus_searching => 'Ricerca di dispositivi USB...';
@override
String usbConnectionFailed(String error) {
return 'Errore nella connessione USB: $error';
}
@override
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
@@ -150,6 +284,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
@override
String get scanner_chromeRequired => 'Browser Chrome richiesto';
@override
String get scanner_chromeRequiredMessage =>
'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.';
@override
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
@@ -233,6 +374,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_longitude => 'Longitudine';
@override
String get settings_contactSettings => 'Impostazioni di contatto';
@override
String get settings_contactSettingsSubtitle =>
'Impostazioni per l\'aggiunta dei contatti';
@override
String get settings_privacyMode => 'Modalità Privacy';
@@ -250,6 +398,52 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
@override
String get settings_privacy => 'Impostazioni sulla privacy';
@override
String get settings_privacySubtitle =>
'Controlla le informazioni che vengono condivise.';
@override
String get settings_privacySettingsDescription =>
'Scegli le informazioni che il tuo dispositivo condivide con gli altri.';
@override
String get settings_denyAll => 'Negare tutto';
@override
String get settings_allowByContact => 'Consenti in base ai flag di contatto';
@override
String get settings_allowAll => 'Consenti tutto';
@override
String get settings_telemetryBaseMode => 'Modalità di base di telemetria';
@override
String get settings_telemetryLocationMode =>
'Modalità di posizionamento telemetrico';
@override
String get settings_telemetryEnvironmentMode =>
'Modalità di ambiente di telemetria';
@override
String get settings_advertLocation => 'Posizione dell\'annuncio';
@override
String get settings_advertLocationSubtitle =>
'Includi la posizione nell\'annuncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
@override
String get settings_actions => 'Azioni';
@@ -547,6 +741,50 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotazione del percorso automatico disabilitata';
@override
String get appSettings_maxRouteWeight =>
'Massimo peso consentito per il percorso';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Il peso massimo che un percorso può accumulare grazie a consegne di successo.';
@override
String get appSettings_initialRouteWeight => 'Peso iniziale del percorso';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Peso di partenza per nuovi percorsi';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Aumento del peso del successo';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Peso aggiunto a un percorso dopo una consegna riuscita.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Riduzione del peso associato al fallimento';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Peso rimosso da un percorso dopo un tentativo di consegna fallito.';
@override
String get appSettings_maxMessageRetries =>
'Numero massimo di tentativi di invio del messaggio';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batteria';
@@ -753,6 +991,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
@override
String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato';
@override
String contacts_groupAlreadyExists(String name) {
return 'Il gruppo \"$name\" esiste già.';
@@ -792,6 +1033,42 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Ultimo visto $days giorni fa';
}
@override
String get contact_info => 'Informazioni di Contatto';
@override
String get contact_settings => 'Impostazioni di contatto';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Ultimo accesso';
@override
String get contact_clearChat => 'Cancella chat';
@override
String get contact_teleBase => 'Base di telemetria';
@override
String get contact_teleBaseSubtitle =>
'Consenti la condivisione del livello della batteria e della telemetria di base';
@override
String get contact_teleLoc => 'Posizione telemetria';
@override
String get contact_teleLocSubtitle =>
'Consenti la condivisione dei dati di posizione';
@override
String get contact_teleEnv => 'Ambiente di telemetria';
@override
String get contact_teleEnvSubtitle =>
'Consenti la condivisione dei dati del sensore ambientale';
@override
String get channels_title => 'Canali';
@@ -1362,6 +1639,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Condividi marcatore qui';
@override
String get map_setAsMyLocation => 'Imposta come la mia posizione';
@override
String get map_pinLabel => 'Etichetta PIN';
@@ -1407,6 +1687,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_otherNodes => 'Altri Nodi';
@override
String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
@override
String get map_keyPrefix => 'Prefisso Chiave';
@@ -1422,6 +1705,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
@override
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
@override
String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery';
@override
String get map_guessedLocation => 'Località indovinata';
@override
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
@@ -1440,6 +1732,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_runTrace => 'Esegui Path Trace';
@override
String get map_runTraceWithReturnPath =>
'Tornare indietro sullo stesso percorso';
@override
String get map_removeLast => 'Rimuovi ultimo';
@@ -2130,6 +2426,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';
@@ -3116,4 +3418,179 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Ultimo accesso';
@override
String get contactsSettings_title => 'Impostazioni dei contatti';
@override
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
@override
String get contactsSettings_otherTitle =>
'Altre impostazioni relative ai contatti';
@override
String get contactsSettings_autoAddUsersTitle =>
'Aggiungere utenti automaticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Aggiungere ripetitori automaticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Aggiungere automaticamente i server delle stanze';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Aggiungere automaticamente i sensori';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sostituisci il più vecchio';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
@override
String get discoveredContacts_Title => 'Contatti scoperti';
@override
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
@override
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
@override
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
@override
String get discoveredContacts_addContact => 'Aggiungi contatto';
@override
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
@override
String get discoveredContacts_deleteContact => 'Elimina Contatto';
@override
String get discoveredContacts_deleteContactAll =>
'Eliminare tutti i contatti scoperti';
@override
String get discoveredContacts_deleteContactAllContent =>
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
@override
String get chat_sendCooldown =>
'Si prega di attendere un momento prima di inviare nuovamente.';
@override
String get appSettings_jumpToOldestUnread =>
'Vai al messaggio più vecchio non letto';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Quando si apre una chat con messaggi non letti, scorrete verso l\'alto fino al primo messaggio non letto, invece che al più recente.';
@override
String get appSettings_languageHu => 'Ungherese';
@override
String get appSettings_languageJa => 'Giapponese';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Statistiche per radio e reti';
@override
String get radioStats_screenTitle => 'Statistiche radio';
@override
String get radioStats_notConnected =>
'Connettiti a un dispositivo per visualizzare le statistiche radio.';
@override
String get radioStats_firmwareTooOld =>
'Le statistiche radio richiedono il firmware versione 8 o successiva.';
@override
String get radioStats_waiting => 'In attesa dei dati…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Livello di rumore: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Ultimo valore RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Ultimo SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tempo di trasmissione in diretta (totale): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tempo di trasmissione RX (totale): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Livello di rumore (dBm) misurato su campioni recenti.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Livello di rumore: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Recupero delle statistiche radio…';
@override
String get radioStats_settingsTile => 'Statistiche radio';
@override
String get radioStats_settingsSubtitle =>
'Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione';
@override
String get scanner_linuxPairingShowPin => 'Mostra PIN';
@override
String get scanner_linuxPairingHidePin => 'Nascondi PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN di associazione Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Inserisci il PIN per $deviceName (lascia vuoto se non ce n\'è).';
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+468
View File
@@ -38,6 +38,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_delete => 'Verwijderen';
@override
String get common_deleteAll => 'Alles verwijderen';
@override
String get common_close => 'Sluiten';
@@ -108,6 +111,134 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Verbind via TCP';
@override
String get tcpHostLabel => 'IP-adres';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Poort';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Voer het eindpunt in en verbind';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Verbinding maken met $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Een IP-adres is vereist.';
@override
String get tcpErrorPortInvalid =>
'De poortwaarde moet tussen 1 en 65535 liggen.';
@override
String get tcpErrorUnsupported =>
'TCP-transport wordt niet ondersteund op deze platform.';
@override
String get tcpErrorTimedOut => 'De TCP-verbinding is verlopen.';
@override
String tcpConnectionFailed(String error) {
return 'Verbinding met TCP mislukt: $error';
}
@override
String get usbScreenTitle => 'Verbind via USB';
@override
String get usbScreenSubtitle =>
'Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
@override
String get usbScreenStatus => 'Selecteer een USB-apparaat';
@override
String get usbScreenNote =>
'USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.';
@override
String get usbScreenEmptyState =>
'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
@override
String get usbErrorPermissionDenied => 'Toegang via USB is geweigerd.';
@override
String get usbErrorDeviceMissing =>
'Het geselecteerde USB-apparaat is niet meer beschikbaar.';
@override
String get usbErrorInvalidPort => 'Selecteer een geldig USB-apparaat.';
@override
String get usbErrorBusy =>
'Een andere verzoek om een USB-verbinding is al in behandeling.';
@override
String get usbErrorNotConnected => 'Er is geen USB-apparaat aangesloten.';
@override
String get usbErrorOpenFailed =>
'Kon het geselecteerde USB-apparaat niet openen.';
@override
String get usbErrorConnectFailed =>
'Kon niet verbinding maken met het geselecteerde USB-apparaat.';
@override
String get usbErrorUnsupported =>
'USB-serieel is niet ondersteund op deze platform.';
@override
String get usbErrorAlreadyActive => 'Een USB-verbinding is al actief.';
@override
String get usbErrorNoDeviceSelected => 'Geen USB-apparaat is geselecteerd.';
@override
String get usbErrorPortClosed => 'De USB-verbinding is niet actief.';
@override
String get usbErrorConnectTimedOut =>
'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.';
@override
String get usbFallbackDeviceName => 'Web-serieapparaat';
@override
String get usbStatus_notConnected => 'Selecteer een USB-apparaat';
@override
String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...';
@override
String get usbStatus_searching => 'Zoeken naar USB-apparaten...';
@override
String usbConnectionFailed(String error) {
return 'Fout bij de USB-verbinding: $error';
}
@override
String get scanner_scanning => 'Scannen naar apparaten...';
@@ -149,6 +280,13 @@ class AppLocalizationsNl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
@override
String get scanner_chromeRequired => 'Chrome-browser vereist';
@override
String get scanner_chromeRequiredMessage =>
'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.';
@override
String get scanner_enableBluetooth => 'Activeer Bluetooth';
@@ -233,6 +371,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_longitude => 'Lengtegraad';
@override
String get settings_contactSettings => 'Contactinstellingen';
@override
String get settings_contactSettingsSubtitle =>
'Instellingen voor het toevoegen van contacten';
@override
String get settings_privacyMode => 'Privacy Mode';
@@ -250,6 +395,50 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld';
@override
String get settings_privacy => 'Privacyinstellingen';
@override
String get settings_privacySubtitle =>
'Beheer welke informatie wordt gedeeld';
@override
String get settings_privacySettingsDescription =>
'Kies welke informatie uw apparaat deelt met anderen';
@override
String get settings_denyAll => 'Weiger alles';
@override
String get settings_allowByContact => 'Toestaan op basis van contactvlaggen';
@override
String get settings_allowAll => 'Alles toestaan';
@override
String get settings_telemetryBaseMode => 'Telemetrie-basismodus';
@override
String get settings_telemetryLocationMode => 'Telemetrie-locatiemodus';
@override
String get settings_telemetryEnvironmentMode => 'Telemetrie-omgevingsmodus';
@override
String get settings_advertLocation => 'Advertentielocatie';
@override
String get settings_advertLocationSubtitle =>
'Locatie opnemen in advertentie';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
@override
String get settings_actions => 'Acties';
@@ -544,6 +733,49 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatische route rotatie is uitgeschakeld';
@override
String get appSettings_maxRouteWeight => 'Maximale gewicht voor de route';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.';
@override
String get appSettings_initialRouteWeight => 'เริ่มต้น gewicht van de route';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Startgewicht voor nieuwe, ontdekte routes';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Toename in het gewicht van het succes';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Gewicht wordt toegevoegd aan een route na een succesvolle levering.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Vermindering van het gewicht van fouten';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Gewicht verwijderd van een pad na een mislukte levering';
@override
String get appSettings_maxMessageRetries =>
'Aantal pogingen om berichten te versturen';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batterij';
@@ -750,6 +982,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
@override
String get contacts_groupNameReserved => 'Deze groepsnaam is gereserveerd';
@override
String contacts_groupAlreadyExists(String name) {
return 'De groep \"$name\" bestaat al.';
@@ -789,6 +1024,40 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Laast gezien $days dagen geleden';
}
@override
String get contact_info => 'Contactinformatie';
@override
String get contact_settings => 'Contactinstellingen';
@override
String get contact_telemetry => 'Telemetrie';
@override
String get contact_lastSeen => 'Laatst gezien';
@override
String get contact_clearChat => 'Chat leegmaken';
@override
String get contact_teleBase => 'Telemetrie_basis';
@override
String get contact_teleBaseSubtitle =>
'Sta delen van batterij niveau en basis telemetrie toe';
@override
String get contact_teleLoc => 'Telemetrielocatie';
@override
String get contact_teleLocSubtitle => 'Locatiegegevens delen toestaan';
@override
String get contact_teleEnv => 'Telemetrieomgeving';
@override
String get contact_teleEnvSubtitle => 'Delen van omgevingsensordata toestaan';
@override
String get channels_title => 'Kanaal';
@@ -1357,6 +1626,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Deel marker hier';
@override
String get map_setAsMyLocation => 'Stel dit in als mijn locatie';
@override
String get map_pinLabel => 'Label vastzetten';
@@ -1402,6 +1674,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Nodes';
@override
String get map_showOverlaps => 'Herhalingssleutel overlapt';
@override
String get map_keyPrefix => 'Prefix sleutel';
@@ -1417,6 +1692,16 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Toon gedeelde markeringen';
@override
String get map_showGuessedLocations =>
'Toon de voorspelde locaties van de knopen';
@override
String get map_showDiscoveryContacts => 'Ontdek contacten weergeven';
@override
String get map_guessedLocation => 'Geroerde locatie';
@override
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
@@ -1436,6 +1721,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_runTrace => 'Padeshulp traceren';
@override
String get map_runTraceWithReturnPath => 'Terugkeren op hetzelfde pad.';
@override
String get map_removeLast => 'Verwijder Laatste';
@@ -2121,6 +2409,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Tijd';
@override
String get repeater_cliQuickClockSync => 'Kloksynchronisatie';
@override
String get repeater_cliQuickDiscovery => 'Ontdek Buren';
@override
String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket';
@@ -3103,4 +3397,178 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Laatst gezien';
@override
String get contactsSettings_title => 'Instellingen voor contacten';
@override
String get contactsSettings_autoAddTitle => 'Automatische detectie';
@override
String get contactsSettings_otherTitle =>
'Andere instellingen voor contactgerelateerde zaken';
@override
String get contactsSettings_autoAddUsersTitle =>
'Gebruikers automatisch toevoegen';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automatisch herhalingstoestellen toevoegen';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Sta toe dat de companion automatisch ontdekte repeaters toevoegt';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatisch kamerservers toevoegen';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automatisch sensoren toevoegen';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Sta toe dat de companion automatisch ontdekte sensoren toevoegt';
@override
String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.';
@override
String get discoveredContacts_Title => 'Ontdekte contacten';
@override
String get discoveredContacts_noMatching => 'Geen overeenkomende contacten';
@override
String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken';
@override
String get discoveredContacts_contactAdded => 'Contact toegevoegd';
@override
String get discoveredContacts_addContact => 'Contact toevoegen';
@override
String get discoveredContacts_copyContact => 'Kopieer contact naar klembord';
@override
String get discoveredContacts_deleteContact => 'Contact verwijderen';
@override
String get discoveredContacts_deleteContactAll =>
'Verwijder alle ontdekte contacten';
@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 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).';
}
}
File diff suppressed because it is too large Load Diff
+475
View File
@@ -38,6 +38,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_delete => 'Excluir';
@override
String get common_deleteAll => 'Excluir Tudo';
@override
String get common_close => 'Fechar';
@@ -108,6 +111,136 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Estabelecer conexão via TCP';
@override
String get tcpHostLabel => 'Endereço IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Porta';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Insira o endereço final e conecte-se.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Conectando a $endpoint...';
}
@override
String get tcpErrorHostRequired => 'É necessário fornecer um endereço IP.';
@override
String get tcpErrorPortInvalid =>
'O valor do porto deve estar entre 1 e 65535.';
@override
String get tcpErrorUnsupported =>
'O protocolo TCP não é suportado nesta plataforma.';
@override
String get tcpErrorTimedOut => 'A conexão TCP expirou.';
@override
String tcpConnectionFailed(String error) {
return 'Falha na conexão TCP: $error';
}
@override
String get usbScreenTitle => 'Conecte via USB';
@override
String get usbScreenSubtitle =>
'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.';
@override
String get usbScreenStatus => 'Selecione um dispositivo USB';
@override
String get usbScreenNote =>
'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.';
@override
String get usbScreenEmptyState =>
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
@override
String get usbErrorPermissionDenied =>
'A permissão para acesso via USB foi negada.';
@override
String get usbErrorDeviceMissing =>
'O dispositivo USB selecionado não está mais disponível.';
@override
String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.';
@override
String get usbErrorBusy =>
'Já existe uma solicitação de conexão USB em andamento.';
@override
String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.';
@override
String get usbErrorOpenFailed =>
'Não foi possível abrir o dispositivo USB selecionado.';
@override
String get usbErrorConnectFailed =>
'Não foi possível conectar ao dispositivo USB selecionado.';
@override
String get usbErrorUnsupported =>
'A comunicação serial via USB não é suportada nesta plataforma.';
@override
String get usbErrorAlreadyActive => 'A conexão USB já está ativa.';
@override
String get usbErrorNoDeviceSelected =>
'Nenhum dispositivo USB foi selecionado.';
@override
String get usbErrorPortClosed => 'A conexão USB não está ativa.';
@override
String get usbErrorConnectTimedOut =>
'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web';
@override
String get usbStatus_notConnected => 'Selecione um dispositivo USB';
@override
String get usbStatus_connecting => 'Conectando ao dispositivo USB...';
@override
String get usbStatus_searching => 'Procurando por dispositivos USB...';
@override
String usbConnectionFailed(String error) {
return 'Falha na conexão USB: $error';
}
@override
String get scanner_scanning => 'Procurando por dispositivos...';
@@ -150,6 +283,13 @@ class AppLocalizationsPt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, ative o Bluetooth para escanear por dispositivos.';
@override
String get scanner_chromeRequired => 'Navegador Chrome necessário';
@override
String get scanner_chromeRequiredMessage =>
'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.';
@override
String get scanner_enableBluetooth => 'Ative o Bluetooth';
@@ -234,6 +374,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Configurações de Contato';
@override
String get settings_contactSettingsSubtitle =>
'Configurações para como os contatos são adicionados';
@override
String get settings_privacyMode => 'Modo de Privacidade';
@@ -251,6 +398,51 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
@override
String get settings_privacy => 'Configurações de Privacidade';
@override
String get settings_privacySubtitle => 'Controle o que é compartilhado.';
@override
String get settings_privacySettingsDescription =>
'Escolha quais informações o seu dispositivo compartilha com os outros.';
@override
String get settings_denyAll => 'Negar todos';
@override
String get settings_allowByContact => 'Permitir por bandeiras de contato';
@override
String get settings_allowAll => 'Permitir todos';
@override
String get settings_telemetryBaseMode => 'Modo Base de Telemetria';
@override
String get settings_telemetryLocationMode =>
'Modo de Localização de Telemetria';
@override
String get settings_telemetryEnvironmentMode =>
'Modo de Ambiente de Telemetria';
@override
String get settings_advertLocation => 'Localização do Anúncio';
@override
String get settings_advertLocationSubtitle =>
'Incluir localização no anúncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
@override
String get settings_actions => 'Ações';
@@ -549,6 +741,49 @@ class AppLocalizationsPt extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotação de roteamento automático desativada';
@override
String get appSettings_maxRouteWeight => 'Peso Máximo da Rota';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.';
@override
String get appSettings_initialRouteWeight => 'Peso Inicial da Rota';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Peso inicial para novos caminhos descobertos';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Aumento do peso para indicar sucesso';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Peso adicionado a um caminho após a entrega bem-sucedida.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Redução do peso da falha';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Peso removido de um caminho após uma tentativa de entrega malsucedida.';
@override
String get appSettings_maxMessageRetries =>
'Número máximo de tentativas de envio de mensagens';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Número de tentativas de reenvio antes de classificar uma mensagem como falha.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Bateria';
@@ -756,6 +991,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
@override
String get contacts_groupNameReserved => 'Este nome de grupo está reservado';
@override
String contacts_groupAlreadyExists(String name) {
return 'O grupo \"$name\" já existe';
@@ -795,6 +1033,42 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Última vez visto $days dias atrás';
}
@override
String get contact_info => 'Informações de Contato';
@override
String get contact_settings => 'Configurações de Contato';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Visto pela última vez';
@override
String get contact_clearChat => 'Limpar Chat';
@override
String get contact_teleBase => 'Base de Telemetria';
@override
String get contact_teleBaseSubtitle =>
'Permitir compartilhamento do nível da bateria e telemetria básica';
@override
String get contact_teleLoc => 'Localização de Telemetria';
@override
String get contact_teleLocSubtitle =>
'Permitir compartilhamento de dados de localização';
@override
String get contact_teleEnv => 'Ambiente de Telemetria';
@override
String get contact_teleEnvSubtitle =>
'Permitir compartilhamento de dados do sensor de ambiente';
@override
String get channels_title => 'Canais';
@@ -1364,6 +1638,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
@override
String get map_setAsMyLocation => 'Defina minha localização';
@override
String get map_pinLabel => 'Rótulo de marcador';
@@ -1409,6 +1686,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_otherNodes => 'Outros Nós';
@override
String get map_showOverlaps => 'Sobreposições da Chave Repeater';
@override
String get map_keyPrefix => 'Prefixo Chave';
@@ -1424,6 +1704,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
@override
String get map_showGuessedLocations =>
'Mostrar as localizações dos nós estimados';
@override
String get map_showDiscoveryContacts => 'Mostrar Contatos de Descoberta';
@override
String get map_guessedLocation => 'Localização estimada';
@override
String get map_lastSeenTime => 'Último Tempo de Visualização';
@@ -1442,6 +1732,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_runTrace => 'Executar Traçado de Caminho';
@override
String get map_runTraceWithReturnPath => 'Retornar ao mesmo caminho.';
@override
String get map_removeLast => 'Remover Último';
@@ -2130,6 +2423,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';
@@ -3111,4 +3410,180 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Visto pela última vez';
@override
String get contactsSettings_title => 'Configurações de contatos';
@override
String get contactsSettings_autoAddTitle => 'Descoberta Automática';
@override
String get contactsSettings_otherTitle =>
'Outras configurações relacionadas a contatos';
@override
String get contactsSettings_autoAddUsersTitle =>
'Adicionar usuários automaticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Permitir que o companheiro adicione automaticamente os usuários descobertos.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Adicionar repetidores automaticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Permitir que o companheiro adicione automaticamente os repetidores descobertos.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Adicionar automaticamente servidores de sala';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Adicionar sensores automaticamente';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Permitir que o companheiro adicione automaticamente sensores descobertos.';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sobrescrever o Mais Antigo';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.';
@override
String get discoveredContacts_Title => 'Contatos Descobertos';
@override
String get discoveredContacts_noMatching => 'Nenhum contato correspondente';
@override
String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos';
@override
String get discoveredContacts_contactAdded => 'Contato adicionado';
@override
String get discoveredContacts_addContact => 'Adicionar Contato';
@override
String get discoveredContacts_copyContact =>
'Copiar Contato para a área de transferência';
@override
String get discoveredContacts_deleteContact => 'Excluir Contato';
@override
String get discoveredContacts_deleteContactAll =>
'Excluir Todos os Contatos Descobertos';
@override
String get discoveredContacts_deleteContactAllContent =>
'Tem certeza de que deseja excluir todos os contatos descobertos?';
@override
String get chat_sendCooldown =>
'Por favor, aguarde um momento antes de reenviar.';
@override
String get appSettings_jumpToOldestUnread =>
'Vá para a mensagem mais antiga não lida';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.';
@override
String get appSettings_languageHu => 'Húngaro';
@override
String get appSettings_languageJa => 'Japonês';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Estatísticas de rádio e malha';
@override
String get radioStats_screenTitle => 'Estatísticas de rádio';
@override
String get radioStats_notConnected =>
'Conecte-se a um dispositivo para visualizar estatísticas de rádio.';
@override
String get radioStats_firmwareTooOld =>
'As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.';
@override
String get radioStats_waiting => 'Aguardando dados…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Nível de ruído: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Último RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Último SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tempo de transmissão da TX (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tempo de uso do RX (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Nível de ruído (dBm) em amostras recentes.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Nível de ruído: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Obtendo estatísticas de rádio…';
@override
String get radioStats_settingsTile => 'Estatísticas de rádio';
@override
String get radioStats_settingsSubtitle =>
'Nível de ruído, RSSI, SNR e tempo de transmissão';
@override
String get 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).';
}
}
+477
View File
@@ -38,6 +38,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get common_delete => 'Удалить';
@override
String get common_deleteAll => 'Удалить все';
@override
String get common_close => 'Закрыть';
@@ -108,6 +111,137 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Установить соединение по протоколу TCP';
@override
String get tcpHostLabel => 'IP-адрес';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Порт';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Введите адрес и подключитесь.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Подключение к $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необходимо указать IP-адрес.';
@override
String get tcpErrorPortInvalid =>
'Порт должен находиться в диапазоне от 1 до 65535.';
@override
String get tcpErrorUnsupported =>
'Протокол TCP не поддерживается на этой платформе.';
@override
String get tcpErrorTimedOut => 'Соединение TCP не удалось установить.';
@override
String tcpConnectionFailed(String error) {
return 'Не удалось установить соединение TCP: $error';
}
@override
String get usbScreenTitle => 'Подключение через USB';
@override
String get usbScreenSubtitle =>
'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.';
@override
String get usbScreenStatus => 'Выберите USB-устройство';
@override
String get usbScreenNote =>
'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.';
@override
String get usbScreenEmptyState =>
'Не обнаружено устройств USB. Подключите одно из них и обновите список.';
@override
String get usbErrorPermissionDenied =>
'Запрос на доступ через USB был отклонен.';
@override
String get usbErrorDeviceMissing =>
'Выбранное USB-устройство больше недоступно.';
@override
String get usbErrorInvalidPort => 'Выберите действительное USB-устройство.';
@override
String get usbErrorBusy =>
'Еще одно запрошенное соединение через USB уже находится в процессе.';
@override
String get usbErrorNotConnected => 'Ни одно USB-устройство не подключено.';
@override
String get usbErrorOpenFailed =>
'Не удалось открыть выбранное USB-устройство.';
@override
String get usbErrorConnectFailed =>
'Не удалось установить соединение с выбранным USB-устройством.';
@override
String get usbErrorUnsupported =>
'Поддержка последовательного USB отсутствует на данной платформе.';
@override
String get usbErrorAlreadyActive => 'USB-соединение уже установлено.';
@override
String get usbErrorNoDeviceSelected =>
'Не было выбрано ни одно устройство USB.';
@override
String get usbErrorPortClosed => 'USB-соединение не установлено.';
@override
String get usbErrorConnectTimedOut =>
'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.';
@override
String get usbFallbackDeviceName =>
'Устройство для последовательного подключения к сети';
@override
String get usbStatus_notConnected => 'Выберите USB-устройство';
@override
String get usbStatus_connecting => 'Подключение к USB-устройству...';
@override
String get usbStatus_searching => 'Поиск USB-устройств...';
@override
String usbConnectionFailed(String error) {
return 'Не удалось установить соединение через USB: $error';
}
@override
String get scanner_scanning => 'Поиск устройств...';
@@ -149,6 +283,13 @@ class AppLocalizationsRu extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
@override
String get scanner_chromeRequired => 'Требуется браузер Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.';
@override
String get scanner_enableBluetooth => 'Включите Bluetooth';
@@ -232,6 +373,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_longitude => 'Долгота';
@override
String get settings_contactSettings => 'Настройки контактов';
@override
String get settings_contactSettingsSubtitle =>
'Настройки добавления контактов';
@override
String get settings_privacyMode => 'Режим конфиденциальности';
@@ -250,6 +398,51 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Режим конфиденциальности выключен';
@override
String get settings_privacy => 'Настройки конфиденциальности';
@override
String get settings_privacySubtitle =>
'Контролируйте, какую информацию делиться.';
@override
String get settings_privacySettingsDescription =>
'Выберите, какую информацию ваше устройство будет делиться с другими.';
@override
String get settings_denyAll => 'Отклонить все';
@override
String get settings_allowByContact => 'Разрешить по флагам контактов';
@override
String get settings_allowAll => 'Разрешить все';
@override
String get settings_telemetryBaseMode => 'Базовый режим телеметрии';
@override
String get settings_telemetryLocationMode =>
'Режим местоположения телеметрии';
@override
String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии';
@override
String get settings_advertLocation => 'Местоположение рекламы';
@override
String get settings_advertLocationSubtitle =>
'Включить местоположение в объявление';
@override
String settings_multiAck(String value) {
return 'Мульти-ACK: $value';
}
@override
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
@override
String get settings_actions => 'Действия';
@@ -548,6 +741,50 @@ class AppLocalizationsRu extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Автоматическое переключение маршрутов отключено';
@override
String get appSettings_maxRouteWeight =>
'Максимальный допустимый вес маршрута';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.';
@override
String get appSettings_initialRouteWeight => 'Начальный вес маршрута';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Начальный вес для новых, только что открытых маршрутов';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Увеличение веса успеха';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Вес, добавленный к маршруту после успешной доставки.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Уменьшение веса неудачи';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Вес, который был удален с пути после неудачной доставки.';
@override
String get appSettings_maxMessageRetries =>
'Максимальное количество повторных попыток отправки сообщения';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батарея';
@@ -754,6 +991,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Имя группы обязательно';
@override
String get contacts_groupNameReserved => 'Это имя группы зарезервировано';
@override
String contacts_groupAlreadyExists(String name) {
return 'Группа \"$name\" уже существует';
@@ -793,6 +1033,42 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Видели $days дн. назад';
}
@override
String get contact_info => 'Контактная информация';
@override
String get contact_settings => 'Настройки контактов';
@override
String get contact_telemetry => 'Телеметрия';
@override
String get contact_lastSeen => 'Последний раз видели';
@override
String get contact_clearChat => 'Очистить чат';
@override
String get contact_teleBase => 'База телеметрии';
@override
String get contact_teleBaseSubtitle =>
'Разрешить обмен уровнем заряда батареи и базовой телеметрией';
@override
String get contact_teleLoc => 'Местоположение телеметрии';
@override
String get contact_teleLocSubtitle =>
'Разрешить обмен данными о местоположении';
@override
String get contact_teleEnv => 'Среда телеметрии';
@override
String get contact_teleEnvSubtitle =>
'Разрешить обмен данными датчиков окружающей среды';
@override
String get channels_title => 'Каналы';
@@ -1365,6 +1641,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Поделиться меткой здесь';
@override
String get map_setAsMyLocation => 'Установить мое местоположение';
@override
String get map_pinLabel => 'Метка';
@@ -1410,6 +1689,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_otherNodes => 'Другие ноды';
@override
String get map_showOverlaps => 'Перекрытия ключа повтора';
@override
String get map_keyPrefix => 'Префикс ключа';
@@ -1425,6 +1707,16 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Показывать общие метки';
@override
String get map_showGuessedLocations =>
'Отобразить предполагаемые места расположения узлов';
@override
String get map_showDiscoveryContacts => 'Показать контакты Discovery';
@override
String get map_guessedLocation => 'Угаданное место';
@override
String get map_lastSeenTime => 'Время последнего появления';
@@ -1443,6 +1735,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_runTrace => 'Запустить трассировку пути';
@override
String get map_runTraceWithReturnPath => 'Вернуться обратно по тому же пути';
@override
String get map_removeLast => 'Удалить последний';
@@ -2132,6 +2427,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 => 'Отправляет пакет анонсирования';
@@ -3123,4 +3424,180 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Последний раз видели';
@override
String get contactsSettings_title => 'Настройки контактов';
@override
String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение';
@override
String get contactsSettings_otherTitle =>
'Другие настройки, связанные с контактами';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматически добавлять пользователей';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженных пользователей';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматически добавлять ретрансляторы';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматически добавлять серверы комнат';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматически добавлять датчики';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженные датчики';
@override
String get contactsSettings_overwriteOldestTitle =>
'Перезаписать самое старое';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.';
@override
String get discoveredContacts_Title => 'Обнаруженные контакты';
@override
String get discoveredContacts_noMatching => 'Нет совпадающих контактов';
@override
String get discoveredContacts_searchHint => 'Найденные контакты поиска';
@override
String get discoveredContacts_contactAdded => 'Контакт добавлен';
@override
String get discoveredContacts_addContact => 'Добавить контакт';
@override
String get discoveredContacts_copyContact =>
'Копировать контакт в буфер обмена';
@override
String get discoveredContacts_deleteContact => 'Удалить контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Удалить Все Обнаруженные Контакты';
@override
String get discoveredContacts_deleteContactAllContent =>
'Вы уверены, что хотите удалить все обнаруженные контакты?';
@override
String get chat_sendCooldown =>
'Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.';
@override
String get appSettings_jumpToOldestUnread =>
'Перейти к самому старому непрочитанному сообщению';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.';
@override
String get appSettings_languageHu => 'Венгерский';
@override
String get appSettings_languageJa => 'Японский';
@override
String get appSettings_languageKo => 'Корейский';
@override
String get radioStats_tooltip => 'Статистика радио и беспроводной сети';
@override
String get radioStats_screenTitle => 'Статистика радиовещания';
@override
String get radioStats_notConnected =>
'Подключитесь к устройству, чтобы просмотреть статистику радио.';
@override
String get radioStats_firmwareTooOld =>
'Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.';
@override
String get radioStats_waiting => 'Ожидаем данных…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Уровень шума: $noiseDbm дБм';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Последнее значение RSSI: $rssiDbm дБм';
}
@override
String radioStats_lastSnr(String snr) {
return 'Последнее значение SNR: $snr дБ';
}
@override
String radioStats_txAir(int seconds) {
return 'Время эфира на телеканале TX (общее): $seconds секунд';
}
@override
String radioStats_rxAir(int seconds) {
return 'Общее время использования RX (в секундах): $seconds с';
}
@override
String get radioStats_chartCaption =>
'Уровень шума (дБм) на основе последних измерений.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Уровень шума: $noiseDbm дБм';
}
@override
String get radioStats_stripWaiting => 'Получение данных о радио…';
@override
String get radioStats_settingsTile => 'Статистика радиовещания';
@override
String get radioStats_settingsSubtitle =>
'Уровень шума, RSSI, SNR и время передачи';
@override
String get 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 (оставьте пустым, если нет).';
}
}
+466
View File
@@ -38,6 +38,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_delete => 'Odstrániť';
@override
String get common_deleteAll => 'Zmazať všetko';
@override
String get common_close => 'Zavrieť';
@@ -108,6 +111,135 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Spojte sa pomocou protokolu TCP';
@override
String get tcpHostLabel => 'IP adresa';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Zadajte cieľovú adresu a pripojte sa.';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Pripojenie k $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Je potrebné zadať IP adresu.';
@override
String get tcpErrorPortInvalid => 'Číslo portu musí byť medzi 1 a 65535.';
@override
String get tcpErrorUnsupported =>
'Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.';
@override
String get tcpErrorTimedOut => 'Pripojenie TCP vypršalo.';
@override
String tcpConnectionFailed(String error) {
return 'Neúspešné vytvorenie TCP spojenia: $error';
}
@override
String get usbScreenTitle => 'Pripojte cez USB';
@override
String get usbScreenSubtitle =>
'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.';
@override
String get usbScreenStatus => 'Vyberte USB zariadenie';
@override
String get usbScreenNote =>
'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.';
@override
String get usbScreenEmptyState =>
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
@override
String get usbErrorPermissionDenied =>
'Žiadosť o prístup cez USB bola zamietnutá.';
@override
String get usbErrorDeviceMissing =>
'Vybrané USB zariadenie už nie je dostupné.';
@override
String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.';
@override
String get usbErrorBusy =>
'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.';
@override
String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.';
@override
String get usbErrorOpenFailed =>
'Nepodarilo sa otvoriť vybrané USB zariadenie.';
@override
String get usbErrorConnectFailed =>
'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.';
@override
String get usbErrorUnsupported =>
'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.';
@override
String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.';
@override
String get usbErrorNoDeviceSelected =>
'Nebolo vybrané žiadne USB zariadenie.';
@override
String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.';
@override
String get usbErrorConnectTimedOut =>
'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.';
@override
String get usbFallbackDeviceName => 'Webový sériový zariadenie';
@override
String get usbStatus_notConnected => 'Vyberte USB zariadenie';
@override
String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...';
@override
String get usbStatus_searching => 'Hľadanie USB zariadení...';
@override
String usbConnectionFailed(String error) {
return 'Neúspešné pripojenie cez USB: $error';
}
@override
String get scanner_scanning => 'Skrívania zariadení...';
@@ -150,6 +282,13 @@ class AppLocalizationsSk extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
@override
String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.';
@override
String get scanner_enableBluetooth => 'Povolte Bluetooth';
@@ -233,6 +372,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_longitude => 'Dĺžka';
@override
String get settings_contactSettings => 'Nastavenia kontaktov';
@override
String get settings_contactSettingsSubtitle =>
'Nastavenia pre pridávanie kontaktov.';
@override
String get settings_privacyMode => 'Režim ochrany súkromia';
@@ -249,6 +395,49 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
@override
String get settings_privacy => 'Nastavenia súkromia';
@override
String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.';
@override
String get settings_privacySettingsDescription =>
'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.';
@override
String get settings_denyAll => 'Zamietnuť všetko';
@override
String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok';
@override
String get settings_allowAll => 'Povoliť všetko';
@override
String get settings_telemetryBaseMode => 'Základný režim telemetrie';
@override
String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie';
@override
String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie';
@override
String get settings_advertLocation => 'Umiestnenie inzerátu';
@override
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
@override
String settings_multiAck(String value) {
return 'Viaceré ACK: $value';
}
@override
String get settings_telemetryModeUpdated =>
'Režim telemetrie bol aktualizovaný';
@override
String get settings_actions => 'Možné akcie';
@@ -541,6 +730,48 @@ class AppLocalizationsSk extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatické prekladanie trás pozastavené';
@override
String get appSettings_maxRouteWeight => 'Maximálna hmotnosť trasy';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.';
@override
String get appSettings_initialRouteWeight => 'Počiatočná váha trasy';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Počiatočná váha pre nové, objavené cesty';
@override
String get appSettings_routeWeightSuccessIncrement => 'Zvyšenie váhy úspechu';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Hmotnosť pridaná k trase po úspešnej doručení';
@override
String get appSettings_routeWeightFailureDecrement =>
'Sníženie váhy, ktorá sa používa na odhad rizika.';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Hmotnosť odstránená z cesty po neúspešnej doručenie';
@override
String get appSettings_maxMessageRetries =>
'Maximalný počet pokusov o doručenie správ';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Počet pokusov o odošleť pred označením správy ako neúspešnej';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batéria';
@@ -748,6 +979,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
@override
String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný';
@override
String contacts_groupAlreadyExists(String name) {
return 'Skupina \"$name\" už existuje';
@@ -789,6 +1023,41 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Posledné zobrazenie $days dní dozadu';
}
@override
String get contact_info => 'Kontaktné informácie';
@override
String get contact_settings => 'Nastavenia kontaktov';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Naposledy videný';
@override
String get contact_clearChat => 'Vymazať chat';
@override
String get contact_teleBase => 'Báza telemetrie';
@override
String get contact_teleBaseSubtitle =>
'Povoliť zdieľanie úrovne batérie a základnej telemetrie';
@override
String get contact_teleLoc => 'Lokácia telemetrie';
@override
String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite';
@override
String get contact_teleEnv => 'Prostredie telemetrie';
@override
String get contact_teleEnvSubtitle =>
'Povoliť zdieľanie údajov senzorov prostredia';
@override
String get channels_title => 'Kanály';
@@ -1358,6 +1627,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Zdieľte značku tu';
@override
String get map_setAsMyLocation => 'Nastavte ako moju polohu';
@override
String get map_pinLabel => 'Označka upozornenia';
@@ -1403,6 +1675,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_otherNodes => 'Ostatné uzly';
@override
String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
@override
String get map_keyPrefix => 'Päťciferné predpona';
@@ -1418,6 +1693,16 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
@override
String get map_showGuessedLocations =>
'Zobraziť umiestnenia odhadnutých uzlov';
@override
String get map_showDiscoveryContacts => 'Zobraziť kontakty objavov';
@override
String get map_guessedLocation => 'Odhadnutá lokalita';
@override
String get map_lastSeenTime => 'Posledný čas sledovania';
@@ -1436,6 +1721,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_runTrace => 'Spustiť trasovaním cesty';
@override
String get map_runTraceWithReturnPath => 'Vráťte sa späť po tej istej ceste.';
@override
String get map_removeLast => 'Odstrániť posledný';
@@ -2118,6 +2406,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.';
@@ -3098,4 +3392,176 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Naposledy videný';
@override
String get contactsSettings_title => 'Nastavenia kontaktov';
@override
String get contactsSettings_autoAddTitle => 'Automatické zisťovanie';
@override
String get contactsSettings_otherTitle =>
'Ďalšie nastavenia súvisiace s kontaktami';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automaticky pridávať užívateľov';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automaticky pridávať opakovače';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavené repeater.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automaticky pridávať server miestnosti';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automaticky pridávať senzory';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavené senzory.';
@override
String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.';
@override
String get discoveredContacts_Title => 'Objavené kontakty';
@override
String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty';
@override
String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty';
@override
String get discoveredContacts_contactAdded => 'Kontakt bol pridaný';
@override
String get discoveredContacts_addContact => 'Pridať kontakt';
@override
String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky';
@override
String get discoveredContacts_deleteContact => 'Zmazať kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Zmazať všetky objavené kontakty';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
@override
String get chat_sendCooldown => 'Prosím, počkajte chvíľu, než zašlete znova.';
@override
String get appSettings_jumpToOldestUnread => 'Presk oceň';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.';
@override
String get appSettings_languageHu => 'Maďarský';
@override
String get appSettings_languageJa => 'Japonský';
@override
String get appSettings_languageKo => 'Kórejský';
@override
String get radioStats_tooltip => 'Statistiky rádiových a sieťových kanálov';
@override
String get radioStats_screenTitle => 'Štatistiky rádiových vysielaní';
@override
String get radioStats_notConnected =>
'Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.';
@override
String get radioStats_firmwareTooOld =>
'Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.';
@override
String get radioStats_waiting => 'Čakám na údaje…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Úroveň hluku: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Posledný údaj RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Posledná hodnota SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Čas vysielania na TX (celkový): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Čas RX (celkový): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Úroveň šumu (dBm) pre posledné vzorky.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Úroveň hluku: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Získavanie údajov o rádiu…';
@override
String get radioStats_settingsTile => 'Štatistiky rádiových vysielaní';
@override
String get radioStats_settingsSubtitle =>
'Úroveň hluku, RSSI, SNR a časové rozloženie';
@override
String get scanner_linuxPairingShowPin => 'Zobraziť PIN';
@override
String get scanner_linuxPairingHidePin => 'Skryť PIN';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth párovací PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Zadajte PIN pre $deviceName (ak nie je, nechajte prázdne).';
}
}
+466
View File
@@ -38,6 +38,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_delete => 'Izbrisati';
@override
String get common_deleteAll => 'Izbriši vse';
@override
String get common_close => 'Zapri';
@@ -108,6 +111,133 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Komunicirajte preko protokola TCP';
@override
String get tcpHostLabel => 'IP naslov';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Vrata';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Vnesite končni naslov in se povežite';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Povezava z $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Potrebna je IP-naslov.';
@override
String get tcpErrorPortInvalid => 'Port mora biti med 1 in 65535.';
@override
String get tcpErrorUnsupported =>
'Transport preko protokola TCP ni podprt na tej platformi.';
@override
String get tcpErrorTimedOut => 'Povezava TCP je presegla časovno obdobje.';
@override
String tcpConnectionFailed(String error) {
return 'Napaka pri povezavi TCP: $error';
}
@override
String get usbScreenTitle => 'Povežite preko USB';
@override
String get usbScreenSubtitle =>
'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.';
@override
String get usbScreenStatus => 'Izberite USB naprave';
@override
String get usbScreenNote =>
'USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.';
@override
String get usbScreenEmptyState =>
'Niti en USB naprave niso najdeni. Povežite eno in posodobite.';
@override
String get usbErrorPermissionDenied =>
'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.';
@override
String get usbErrorDeviceMissing => 'Izbrani USB napravej je več ne.';
@override
String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.';
@override
String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.';
@override
String get usbErrorNotConnected => 'Ni priklopljenih USB naprave.';
@override
String get usbErrorOpenFailed =>
'Uspešno ni bilo mogo, da se odpre izbran naprave USB.';
@override
String get usbErrorConnectFailed =>
'Niso bilo mogoče uskladiti povezave z izbranim USB napom.';
@override
String get usbErrorUnsupported =>
'USB serijska komunikacija ni podprta na tej platformi.';
@override
String get usbErrorAlreadyActive => 'USB povezava je že aktivirana.';
@override
String get usbErrorNoDeviceSelected => 'Ni bilo izbranega USB naprave.';
@override
String get usbErrorPortClosed => 'USB povezava ni aktivirana.';
@override
String get usbErrorConnectTimedOut =>
'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.';
@override
String get usbFallbackDeviceName =>
'Naprave za serijsko komunikacijo preko spleta';
@override
String get usbStatus_notConnected => 'Izberite USB naprave.';
@override
String get usbStatus_connecting => 'Povezava z USB napravo...';
@override
String get usbStatus_searching => 'Iskanje USB naprav...';
@override
String usbConnectionFailed(String error) {
return 'Napaka pri povezavi preko USB: $error';
}
@override
String get scanner_scanning => 'Skeniram za naprave...';
@@ -150,6 +280,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
@override
String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.';
@override
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
@@ -233,6 +370,13 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_longitude => 'Dolžina';
@override
String get settings_contactSettings => 'Nastavitve stika';
@override
String get settings_contactSettingsSubtitle =>
'Nastavitve za dodajanje stikov.';
@override
String get settings_privacyMode => 'Zasebnost';
@@ -249,6 +393,50 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
@override
String get settings_privacy => 'Nastavitve zasebnosti';
@override
String get settings_privacySubtitle =>
'Kontrolirajte, katere informacije so deljene.';
@override
String get settings_privacySettingsDescription =>
'Izberite, katere informacije vaš naprava deli z drugimi.';
@override
String get settings_denyAll => 'Zavrniti vse';
@override
String get settings_allowByContact => 'Dovoli po kontaktnih zastavah';
@override
String get settings_allowAll => 'Dovoli vse';
@override
String get settings_telemetryBaseMode => 'Osnovni način telemetrije';
@override
String get settings_telemetryLocationMode => 'Način delovanja telemetrije';
@override
String get settings_telemetryEnvironmentMode =>
'Način delovanja okolja telemetrije';
@override
String get settings_advertLocation => 'Lokacija oglasa';
@override
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
@override
String settings_multiAck(String value) {
return 'Večkratni potrditvi: $value';
}
@override
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
@override
String get settings_actions => 'Akcije';
@@ -543,6 +731,49 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Samodejno krmilno rotiranje je onemogočeno';
@override
String get appSettings_maxRouteWeight => 'Največja dovoljena teža poti';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.';
@override
String get appSettings_initialRouteWeight => 'Izvirna teža poti';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Izguba teže za nove, odkriti poti';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Učinkovitost: povečanje';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Težava, dodana poti po uspešni dostavi';
@override
String get appSettings_routeWeightFailureDecrement =>
'Zmanjšanje teže, ki je povezana s pomanjkanjem';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Težo, ki ni bila uspešno dostavljena, odstranili s poti.';
@override
String get appSettings_maxMessageRetries =>
'Najve število poskusov pošiljanja sporočil';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Baterija';
@@ -748,6 +979,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
@override
String get contacts_groupNameReserved => 'To ime skupine je rezervirano';
@override
String contacts_groupAlreadyExists(String name) {
return 'Skupina \"$name\" že obstaja';
@@ -787,6 +1021,41 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Zadnjič viden pred $days dnem';
}
@override
String get contact_info => 'Kontaktni podatki';
@override
String get contact_settings => 'Nastavitve stika';
@override
String get contact_telemetry => 'Telemetrija';
@override
String get contact_lastSeen => 'Zadnjič videno';
@override
String get contact_clearChat => 'Počisti klepet';
@override
String get contact_teleBase => 'Baza telemetrije';
@override
String get contact_teleBaseSubtitle =>
'Dovoli deljenje stanja baterije in osnovne telemetrije';
@override
String get contact_teleLoc => 'Lokacija telemetrije';
@override
String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji';
@override
String get contact_teleEnv => 'Okolje telemetrije';
@override
String get contact_teleEnvSubtitle =>
'Dovoli deljenje podatkov okoljskih senzorjev';
@override
String get channels_title => 'Kanali';
@@ -1354,6 +1623,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Delite točke tukaj.';
@override
String get map_setAsMyLocation => 'Nastavite to kot mojo lokacijo';
@override
String get map_pinLabel => 'Oznaka za pritrditev';
@@ -1399,6 +1671,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_otherNodes => 'Druge vozlišča';
@override
String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
@override
String get map_keyPrefix => 'Predpona ključa';
@@ -1414,6 +1689,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Pokaži skupno označenja';
@override
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
@override
String get map_showDiscoveryContacts => 'Prikaži odkritja kontaktov';
@override
String get map_guessedLocation => 'Predpostavljena lokacija';
@override
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
@@ -1432,6 +1716,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_runTrace => 'Zaženi sledenje poti';
@override
String get map_runTraceWithReturnPath => 'Vrni se nazaj po isti poti.';
@override
String get map_removeLast => 'Odstrani Zadnji';
@@ -2122,6 +2409,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';
@@ -3103,4 +3396,177 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Zadnjič videno';
@override
String get contactsSettings_title => 'Nastavitve stikov';
@override
String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje';
@override
String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki';
@override
String get contactsSettings_autoAddUsersTitle =>
'Avtomatsko dodaj uporabnike';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Avtomatsko dodaj ponovitelje';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Avtomatsko dodaj strežnike sob';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Avtomatsko dodaj senzorje';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.';
@override
String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.';
@override
String get discoveredContacts_Title => 'Odkriti stiki';
@override
String get discoveredContacts_noMatching => 'Ni ujemajočih stikov';
@override
String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju';
@override
String get discoveredContacts_contactAdded => 'Kontakt dodan';
@override
String get discoveredContacts_addContact => 'Dodaj stik';
@override
String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče';
@override
String get discoveredContacts_deleteContact => 'Izbriši stik';
@override
String get discoveredContacts_deleteContactAll =>
'Izbriši vse odkrite kontakte';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
@override
String get chat_sendCooldown =>
'Prosimo, počakajte trenutek, preden pošljete ponovno.';
@override
String get appSettings_jumpToOldestUnread =>
'Pritisnite za najstarejše nepročitano sporočilo';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.';
@override
String get appSettings_languageHu => 'Madžarski';
@override
String get appSettings_languageJa => 'Japonski';
@override
String get appSettings_languageKo => 'Korejski';
@override
String get radioStats_tooltip => 'Statistike za radio in mrežo';
@override
String get radioStats_screenTitle => 'Radijske statistike';
@override
String get radioStats_notConnected =>
'Povežite se z napravo, da si ogledate statistiko o radiju.';
@override
String get radioStats_firmwareTooOld =>
'Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.';
@override
String get radioStats_waiting => 'Čakam na podatke…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Število šuma: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Najkasnejše vrednost RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Najkasnejše vrednost SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Čas na TX (skupno): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Čas, namenjen RX-ju (skupno): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ravnovredna raven šuma (dBm) za nedavne vzorce.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Število šuma: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Prejemanje statistike o radiju…';
@override
String get radioStats_settingsTile => 'Radijske statistike';
@override
String get radioStats_settingsSubtitle =>
'Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema';
@override
String get 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).';
}
}
+465
View File
@@ -38,6 +38,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_delete => 'Radera';
@override
String get common_deleteAll => 'Ta bort alla';
@override
String get common_close => 'Stänga';
@@ -108,6 +111,133 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'Anslut via TCP';
@override
String get tcpHostLabel => 'IP-adress';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Ange slutpunkt och anslut';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Anslutning till $endpoint...';
}
@override
String get tcpErrorHostRequired => 'IP-adress krävs.';
@override
String get tcpErrorPortInvalid => 'Porten måste vara mellan 1 och 65535.';
@override
String get tcpErrorUnsupported =>
'TCP-transport fungerar inte på denna plattform.';
@override
String get tcpErrorTimedOut => 'TCP-anslutningen har tidsut gått.';
@override
String tcpConnectionFailed(String error) {
return 'Fel vid TCP-anslutning: $error';
}
@override
String get usbScreenTitle => 'Anslut via USB';
@override
String get usbScreenSubtitle =>
'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.';
@override
String get usbScreenStatus => 'Välj en USB-enhet';
@override
String get usbScreenNote =>
'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.';
@override
String get usbScreenEmptyState =>
'Inga USB-enheter hittades. Anslut en och uppdatera.';
@override
String get usbErrorPermissionDenied => 'Tillgången via USB nekas.';
@override
String get usbErrorDeviceMissing =>
'Den valda USB-enheten är inte längre tillgänglig.';
@override
String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.';
@override
String get usbErrorBusy =>
'En annan förfrågan om USB-anslutning är redan pågående.';
@override
String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.';
@override
String get usbErrorOpenFailed =>
'Misslyckades med att öppna det valda USB-enheten.';
@override
String get usbErrorConnectFailed =>
'Kunde inte ansluta till det valda USB-enheten.';
@override
String get usbErrorUnsupported =>
'USB-seriell kommunikation stöds inte på denna plattform.';
@override
String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.';
@override
String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.';
@override
String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.';
@override
String get usbErrorConnectTimedOut =>
'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.';
@override
String get usbFallbackDeviceName => 'Web-serieenhet';
@override
String get usbStatus_notConnected => 'Välj en USB-enhet';
@override
String get usbStatus_connecting => 'Anslutning till USB-enhet...';
@override
String get usbStatus_searching => 'Söker efter USB-enheter...';
@override
String usbConnectionFailed(String error) {
return 'Fel vid USB-anslutning: $error';
}
@override
String get scanner_scanning => 'Söker efter enheter...';
@@ -149,6 +279,13 @@ class AppLocalizationsSv extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Vänligen aktivera Bluetooth för att söka efter enheter.';
@override
String get scanner_chromeRequired => 'Chrome-webbläsare krävs';
@override
String get scanner_chromeRequiredMessage =>
'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.';
@override
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
@@ -232,6 +369,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_longitude => 'Längdgrad';
@override
String get settings_contactSettings => 'Kontaktinställningar';
@override
String get settings_contactSettingsSubtitle =>
'Inställningar för hur kontakter läggs till.';
@override
String get settings_privacyMode => 'Privatläge';
@@ -248,6 +392,49 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privatläge är avstängt';
@override
String get settings_privacy => 'Inställningar för sekretess';
@override
String get settings_privacySubtitle =>
'Kontrollera vilken information som delas.';
@override
String get settings_privacySettingsDescription =>
'Välj vilken information din enhet delar med andra.';
@override
String get settings_denyAll => 'Neka alla';
@override
String get settings_allowByContact => 'Tillåt via kontaktflaggor';
@override
String get settings_allowAll => 'Tillåt alla';
@override
String get settings_telemetryBaseMode => 'Telemetribasläge';
@override
String get settings_telemetryLocationMode => 'Telemetritillstånd för plats';
@override
String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge';
@override
String get settings_advertLocation => 'Annonsplacering';
@override
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
@override
String get settings_actions => 'Åtgärder';
@@ -538,6 +725,48 @@ class AppLocalizationsSv extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatisk ruttrotation är avstängd';
@override
String get appSettings_maxRouteWeight => 'Maximalt tillåtet vikt för rutten';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.';
@override
String get appSettings_initialRouteWeight => 'Initial vikt för rutt';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Initial vikt för nyligen upptäckta vägar';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Ökning av vikt för framgång';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Vikt läggs till en väg efter en lyckad leverans.';
@override
String get appSettings_routeWeightFailureDecrement =>
'Minskning av vikten för misslyckande';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Vikt som tagits bort från en väg efter ett misslyckat leveransförsök';
@override
String get appSettings_maxMessageRetries => 'Maximalt antal försök';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Batteri';
@@ -744,6 +973,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
@override
String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat';
@override
String contacts_groupAlreadyExists(String name) {
return 'Gruppen \"$name\" finns redan.';
@@ -783,6 +1015,40 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Senast synlig $days dagar sedan';
}
@override
String get contact_info => 'Kontaktinformation';
@override
String get contact_settings => 'Kontaktinställningar';
@override
String get contact_telemetry => 'Telemetri';
@override
String get contact_lastSeen => 'Senast sedd';
@override
String get contact_clearChat => 'Rensa Chatt';
@override
String get contact_teleBase => 'Telemetribas';
@override
String get contact_teleBaseSubtitle =>
'Tillåt delning av batterinivå och grundläggande telemetri';
@override
String get contact_teleLoc => 'Telemetridata plats';
@override
String get contact_teleLocSubtitle => 'Tillåt delning av platsdata';
@override
String get contact_teleEnv => 'Telemetri Miljö';
@override
String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata';
@override
String get channels_title => 'Kanaler';
@@ -1350,6 +1616,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Dela markeringen här';
@override
String get map_setAsMyLocation => 'Ange som min plats';
@override
String get map_pinLabel => 'Fästetikett';
@@ -1395,6 +1664,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_otherNodes => 'Andra noder';
@override
String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
@override
String get map_keyPrefix => 'Nyckelprefix';
@@ -1410,6 +1682,16 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Visa delade markörer';
@override
String get map_showGuessedLocations =>
'Visa upp de antagna nodernas placeringar';
@override
String get map_showDiscoveryContacts => 'Visa Discovery-kontakter';
@override
String get map_guessedLocation => 'Gissad plats';
@override
String get map_lastSeenTime => 'Senaste Visats Tid';
@@ -1428,6 +1710,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_runTrace => 'Kör spårsökning';
@override
String get map_runTraceWithReturnPath => 'Gå tillbaka på samma väg';
@override
String get map_removeLast => 'Ta bort sista';
@@ -2109,6 +2394,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';
@@ -3081,4 +3372,178 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Senast sedd';
@override
String get contactsSettings_title => 'Kontaktinställningar';
@override
String get contactsSettings_autoAddTitle => 'Automatisk upptäckt';
@override
String get contactsSettings_otherTitle =>
'Andra inställningar relaterade till kontakt';
@override
String get contactsSettings_autoAddUsersTitle =>
'Lägg till användare automatiskt';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta användare';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Lägg till upprepande enheter automatiskt';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Lägg automatiskt till rumsservrar';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Lägg till sensorer automatiskt';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.';
@override
String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.';
@override
String get discoveredContacts_Title => 'Upptäckta kontakter';
@override
String get discoveredContacts_noMatching => 'Inga matchande kontakter';
@override
String get discoveredContacts_searchHint => 'Sök uppfunna kontakter';
@override
String get discoveredContacts_contactAdded => 'Kontakt tillagd';
@override
String get discoveredContacts_addContact => 'Lägg till kontakt';
@override
String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp';
@override
String get discoveredContacts_deleteContact => 'Ta bort kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Ta bort alla upptäckta kontakter';
@override
String get discoveredContacts_deleteContactAllContent =>
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
@override
String get chat_sendCooldown =>
'Vänligen vänta en stund innan du skickar igen.';
@override
String get appSettings_jumpToOldestUnread =>
'Gå direkt till det äldsta, obesvarade meddelandet';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.';
@override
String get appSettings_languageHu => 'Ungerskt';
@override
String get appSettings_languageJa => 'Japanska';
@override
String get appSettings_languageKo => 'Koreanska';
@override
String get radioStats_tooltip => 'Radio- och mesh-statistik';
@override
String get radioStats_screenTitle => 'Radiostation';
@override
String get radioStats_notConnected =>
'Anslut till en enhet för att visa radiostatistik.';
@override
String get radioStats_firmwareTooOld =>
'Radio statistik kräver kompatibel firmware version 8 eller senare.';
@override
String get radioStats_waiting => 'Väntar på data…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Bakgrundsnivå: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Senaste RSSI-värde: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Senaste SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX-tid (total): $seconds sekunder';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX-tid (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ljudnivå (dBm) baserat på de senaste mätningarna.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Bakgrundsnivå: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Hämtar radiostatistik…';
@override
String get radioStats_settingsTile => 'Radiostation';
@override
String get radioStats_settingsSubtitle =>
'Bakgrundsnivå, RSSI, SNR och tillgänglig tid';
@override
String get 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).';
}
}
+473
View File
@@ -38,6 +38,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get common_delete => 'Видалити';
@override
String get common_deleteAll => 'Видалити все';
@override
String get common_close => 'Закрити';
@@ -108,6 +111,135 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => 'Bluetooth';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => 'З\'єднатися через протокол TCP';
@override
String get tcpHostLabel => 'IP-адреса';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Порт';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Введіть кінцеву точку та підключіться';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Підключення до $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Необхідно вказати IP-адресу.';
@override
String get tcpErrorPortInvalid => 'Порт повинен бути в межах від 1 до 65535.';
@override
String get tcpErrorUnsupported =>
'Транспорт TCP не підтримується на цій платформі.';
@override
String get tcpErrorTimedOut =>
'З\'єднання TCP завершилося через закінчення часу очікування.';
@override
String tcpConnectionFailed(String error) {
return 'Не вдалося встановити з\'єднання TCP: $error';
}
@override
String get usbScreenTitle => 'Підключити через USB';
@override
String get usbScreenSubtitle =>
'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.';
@override
String get usbScreenStatus => 'Виберіть пристрій USB';
@override
String get usbScreenNote =>
'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.';
@override
String get usbScreenEmptyState =>
'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.';
@override
String get usbErrorPermissionDenied =>
'Було відмовлено у наданні дозволу на використання USB.';
@override
String get usbErrorDeviceMissing => 'Вибране USB-пристрій більше недоступне.';
@override
String get usbErrorInvalidPort => 'Виберіть дійсний USB-пристрій.';
@override
String get usbErrorBusy =>
'Ще один запит на підключення через USB вже обробляється.';
@override
String get usbErrorNotConnected => 'Немає підключених пристроїв USB.';
@override
String get usbErrorOpenFailed => 'Не вдалося відкрити вибране USB-пристрій.';
@override
String get usbErrorConnectFailed =>
'Не вдалося підключитися до вибраного USB-пристрою.';
@override
String get usbErrorUnsupported =>
'Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.';
@override
String get usbErrorAlreadyActive => 'USB-з\'єднання вже встановлено.';
@override
String get usbErrorNoDeviceSelected =>
'Не було вибрано жодного пристрою USB.';
@override
String get usbErrorPortClosed => 'З\'єднання USB не встановлено.';
@override
String get usbErrorConnectTimedOut =>
'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.';
@override
String get usbFallbackDeviceName =>
'Пристрій для передачі даних по веб-серіалах';
@override
String get usbStatus_notConnected => 'Виберіть пристрій USB';
@override
String get usbStatus_connecting => 'Підключення до USB-пристрою...';
@override
String get usbStatus_searching => 'Пошук пристроїв USB...';
@override
String usbConnectionFailed(String error) {
return 'Не вдалося встановити з\'єднання через USB: $error';
}
@override
String get scanner_scanning => 'Пошук пристроїв...';
@@ -150,6 +282,13 @@ class AppLocalizationsUk extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
@override
String get scanner_chromeRequired => 'Потрібен браузер Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.';
@override
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
@@ -232,6 +371,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_longitude => 'Довгота';
@override
String get settings_contactSettings => 'Налаштування контактів';
@override
String get settings_contactSettingsSubtitle =>
'Налаштування для додавання контактів';
@override
String get settings_privacyMode => 'Режим приватності';
@@ -249,6 +395,50 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Режим приватності вимкнено';
@override
String get settings_privacy => 'Налаштування приватності';
@override
String get settings_privacySubtitle =>
'Керуйте інформацією, яку буде спільно використовуватися';
@override
String get settings_privacySettingsDescription =>
'Виберіть, яку інформацію ваш пристрій буде передавати іншим.';
@override
String get settings_denyAll => 'Відхилити все';
@override
String get settings_allowByContact => 'Дозволити за контактними прапорцями';
@override
String get settings_allowAll => 'Дозволити все';
@override
String get settings_telemetryBaseMode => 'Режим базової телеметрії';
@override
String get settings_telemetryLocationMode => 'Режим місця телеметрії';
@override
String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії';
@override
String get settings_advertLocation => 'Розміщення реклами';
@override
String get settings_advertLocationSubtitle =>
'Включити місце розташування в оголошення';
@override
String settings_multiAck(String value) {
return 'Багатократне підтвердження: $value';
}
@override
String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
@override
String get settings_actions => 'Дії';
@@ -546,6 +736,49 @@ class AppLocalizationsUk extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Авторотація маршрутизації вимкнена';
@override
String get appSettings_maxRouteWeight => 'Максимальна вага маршруту';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.';
@override
String get appSettings_initialRouteWeight => 'Початкова вартість маршруту';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Початкова вага для нових відкритих шляхів';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Збільшення ваги успіху';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Вага, додана до маршруту після успішної доставки';
@override
String get appSettings_routeWeightFailureDecrement =>
'Зменшення ваги помилки';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Вага, яка була знята з маршруту після невдалої доставки';
@override
String get appSettings_maxMessageRetries =>
'Максимальна кількість повторних спроб надсилання повідомлення';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => 'Батарея';
@@ -752,6 +985,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
@override
String get contacts_groupNameReserved => 'Ця назва групи зарезервована';
@override
String contacts_groupAlreadyExists(String name) {
return 'Група «$name» вже існує.';
@@ -791,6 +1027,42 @@ class AppLocalizationsUk extends AppLocalizations {
return 'В мережі $days дн. тому';
}
@override
String get contact_info => 'Контактна інформація';
@override
String get contact_settings => 'Налаштування контактів';
@override
String get contact_telemetry => 'Телеметрія';
@override
String get contact_lastSeen => 'Останній раз бачили';
@override
String get contact_clearChat => 'Очистити чат';
@override
String get contact_teleBase => 'Базовий телебачення';
@override
String get contact_teleBaseSubtitle =>
'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії';
@override
String get contact_teleLoc => 'Розташування телеметрії';
@override
String get contact_teleLocSubtitle =>
'Дозволити спільне використання даних про місцеположення';
@override
String get contact_teleEnv => 'Середовище телеметрії';
@override
String get contact_teleEnvSubtitle =>
'Дозволити спільний доступ до даних датчиків середовища';
@override
String get channels_title => 'Канали';
@@ -1364,6 +1636,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Поділитися маркером тут';
@override
String get map_setAsMyLocation => 'Встановити моє місцезнаходження';
@override
String get map_pinLabel => 'Мітка піна';
@@ -1409,6 +1684,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_otherNodes => 'Інші вузли';
@override
String get map_showOverlaps => 'Перекриття ключа повторювача';
@override
String get map_keyPrefix => 'Префікс ключа';
@@ -1424,6 +1702,16 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Показувати спільні маркери';
@override
String get map_showGuessedLocations =>
'Показати місцезнаходження передбачених вузлів';
@override
String get map_showDiscoveryContacts => 'Показати контакти Відкриття';
@override
String get map_guessedLocation => 'Визначено місцезнаходження';
@override
String get map_lastSeenTime => 'Час останньої активності';
@@ -1442,6 +1730,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_runTrace => 'Виконати трасування шляху';
@override
String get map_runTraceWithReturnPath => 'Повернутися назад тим же шляхом';
@override
String get map_removeLast => 'Видалити останній';
@@ -2136,6 +2427,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 => 'Надсилає пакет оголошення';
@@ -3130,4 +3427,180 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Останній раз бачили';
@override
String get contactsSettings_title => 'Налаштування контактів';
@override
String get contactsSettings_autoAddTitle => 'Автоматичне виявлення';
@override
String get contactsSettings_otherTitle =>
'Інші налаштування, пов\'язані з контактами';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматично додавати користувачів';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Дозволити супутникові автоматично додавати виявлених користувачів';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматично додавати повторювачі';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Дозволити супутнику автоматично додавати виявлені ретранслятори';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматично додавати сервери кімнат';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматично додавати датчики';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Дозволити супровіднику автоматично додавати виявлені сенсори';
@override
String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.';
@override
String get discoveredContacts_Title => 'Виявлені контакти';
@override
String get discoveredContacts_noMatching =>
'Відповідних контактів не знайдено';
@override
String get discoveredContacts_searchHint => 'Знайти виявлені контакти';
@override
String get discoveredContacts_contactAdded => 'Контакт додано';
@override
String get discoveredContacts_addContact => 'Додати контакт';
@override
String get discoveredContacts_copyContact =>
'Копіювати контакт у буфер обміну';
@override
String get discoveredContacts_deleteContact => 'Видалити контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Видалити всі виявлені контакти';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ви впевнені, що хочете видалити всі виявлені контакти?';
@override
String get chat_sendCooldown =>
'Будь ласка, зачекайте трохи, перш ніж відправляти знову.';
@override
String get appSettings_jumpToOldestUnread =>
'Перейти до найстарішого непрочитаного повідомлення';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.';
@override
String get appSettings_languageHu => 'Угорський';
@override
String get appSettings_languageJa => 'Японська';
@override
String get appSettings_languageKo => 'Кореєська';
@override
String get radioStats_tooltip => 'Статистика радіо та мережі';
@override
String get radioStats_screenTitle => 'Дані про радіостанції';
@override
String get radioStats_notConnected =>
'Підключіться до пристрою, щоб переглядати статистику радіопередач.';
@override
String get radioStats_firmwareTooOld =>
'Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.';
@override
String get radioStats_waiting => 'Очікую на отримання даних…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Рівень шуму: $noiseDbm дБм';
}
@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 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 (залиште порожнім, якщо його немає).';
}
}
+428
View File
@@ -38,6 +38,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get common_delete => '删除';
@override
String get common_deleteAll => '删除全部';
@override
String get common_close => '关闭';
@@ -108,6 +111,123 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanner_title => '连接设备';
@override
String get connectionChoiceUsbLabel => 'USB';
@override
String get connectionChoiceBluetoothLabel => '蓝牙';
@override
String get connectionChoiceTcpLabel => 'TCP';
@override
String get tcpScreenTitle => '通过 TCP 连接';
@override
String get tcpHostLabel => 'IP地址';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => '端口';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => '输入目标地址,然后连接';
@override
String tcpStatus_connectingTo(String endpoint) {
return '连接到 $endpoint...';
}
@override
String get tcpErrorHostRequired => '需要提供IP地址。';
@override
String get tcpErrorPortInvalid => '端口号必须在 1 到 65535 之间。';
@override
String get tcpErrorUnsupported => '此平台不支持 TCP 传输。';
@override
String get tcpErrorTimedOut => 'TCP 连接超时。';
@override
String tcpConnectionFailed(String error) {
return 'TCP 连接失败:$error';
}
@override
String get usbScreenTitle => '通过USB连接';
@override
String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。';
@override
String get usbScreenStatus => '选择一个 USB 设备';
@override
String get usbScreenNote => 'USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。';
@override
String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
@override
String get usbErrorPermissionDenied => '拒绝了USB权限。';
@override
String get usbErrorDeviceMissing => '所选的USB设备已不再可用。';
@override
String get usbErrorInvalidPort => '选择一个有效的USB设备。';
@override
String get usbErrorBusy => '还有一个 USB 连接请求正在进行中。';
@override
String get usbErrorNotConnected => '没有连接任何USB设备。';
@override
String get usbErrorOpenFailed => '未能打开所选的USB设备。';
@override
String get usbErrorConnectFailed => '未能连接到所选的USB设备。';
@override
String get usbErrorUnsupported => '此平台不支持USB串行通信。';
@override
String get usbErrorAlreadyActive => 'USB 连接已建立。';
@override
String get usbErrorNoDeviceSelected => '未选择任何 USB 设备。';
@override
String get usbErrorPortClosed => 'USB 连接未建立。';
@override
String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。';
@override
String get usbFallbackDeviceName => 'Web 串流设备';
@override
String get usbStatus_notConnected => '选择一个 USB 设备';
@override
String get usbStatus_connecting => '连接USB设备...';
@override
String get usbStatus_searching => '正在搜索 USB 设备...';
@override
String usbConnectionFailed(String error) {
return 'USB 连接失败:$error';
}
@override
String get scanner_scanning => '正在搜索设备...';
@@ -148,6 +268,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备';
@override
String get scanner_chromeRequired => '需要 Chrome 浏览器';
@override
String get scanner_chromeRequiredMessage =>
'此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。';
@override
String get scanner_enableBluetooth => '启用蓝牙';
@@ -226,6 +353,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_longitude => '经度';
@override
String get settings_contactSettings => '联系人设置';
@override
String get settings_contactSettingsSubtitle => '添加联系人的设置';
@override
String get settings_privacyMode => '隐私模式';
@@ -241,6 +374,47 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_privacyModeDisabled => '隐私模式已关闭';
@override
String get settings_privacy => '隐私设置';
@override
String get settings_privacySubtitle => '控制要共享的信息。';
@override
String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。';
@override
String get settings_denyAll => '拒绝所有';
@override
String get settings_allowByContact => '按联系人标志允许';
@override
String get settings_allowAll => '允许全部';
@override
String get settings_telemetryBaseMode => '遥测基础模式';
@override
String get settings_telemetryLocationMode => '遥测位置模式';
@override
String get settings_telemetryEnvironmentMode => '遥测环境模式';
@override
String get settings_advertLocation => '广告位置';
@override
String get settings_advertLocationSubtitle => '在广告中包含位置';
@override
String settings_multiAck(String value) {
return '多重ACK$value';
}
@override
String get settings_telemetryModeUpdated => '遥测模式已更新';
@override
String get settings_actions => '操作';
@@ -515,6 +689,43 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用';
@override
String get appSettings_maxRouteWeight => '最大路径重量';
@override
String get appSettings_maxRouteWeightSubtitle => '一条路径可以累积的最大重量,取决于成功交付的数量。';
@override
String get appSettings_initialRouteWeight => '初始路线权重';
@override
String get appSettings_initialRouteWeightSubtitle => '新发现路径的初始重量';
@override
String get appSettings_routeWeightSuccessIncrement => '成功权重增加';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'在成功交付后,将重量添加到路径中';
@override
String get appSettings_routeWeightFailureDecrement => '失败权重降低';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'从一条路径上移除的货物,由于无法成功交付而移除。';
@override
String get appSettings_maxMessageRetries => '最大消息重试次数';
@override
String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
@override
String path_routeWeight(String weight, String max) {
return '$weight/$max';
}
@override
String get appSettings_battery => '电池';
@@ -712,6 +923,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_groupNameRequired => '请输入群聊名称';
@override
String get contacts_groupNameReserved => '该群组名称已被保留';
@override
String contacts_groupAlreadyExists(String name) {
return '名为 \"$name\" 的群聊已存在';
@@ -750,6 +964,39 @@ class AppLocalizationsZh extends AppLocalizations {
return '最后在线 $days 天前';
}
@override
String get contact_info => '联系信息';
@override
String get contact_settings => '联系人设置';
@override
String get contact_telemetry => '遥测数据';
@override
String get contact_lastSeen => '最近出现';
@override
String get contact_clearChat => '清除聊天记录';
@override
String get contact_teleBase => '遥测基站';
@override
String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据';
@override
String get contact_teleLoc => '遥测位置';
@override
String get contact_teleLocSubtitle => '允许共享位置数据';
@override
String get contact_teleEnv => '遥测环境';
@override
String get contact_teleEnvSubtitle => '允许共享环境传感器数据';
@override
String get channels_title => '频道';
@@ -1288,6 +1535,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_shareMarkerHere => '在此分享标记';
@override
String get map_setAsMyLocation => '设置为我的位置';
@override
String get map_pinLabel => '标签';
@@ -1332,6 +1582,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_otherNodes => '其他节点';
@override
String get map_showOverlaps => '重复键重叠';
@override
String get map_keyPrefix => '关键字前缀';
@@ -1347,6 +1600,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_showSharedMarkers => '显示共享标记';
@override
String get map_showGuessedLocations => '显示猜测的节点位置';
@override
String get map_showDiscoveryContacts => '显示发现联系人';
@override
String get map_guessedLocation => '猜测的位置';
@override
String get map_lastSeenTime => '最后在线时间';
@@ -1365,6 +1627,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_runTrace => '运行路径追踪';
@override
String get map_runTraceWithReturnPath => '沿着相同的路径返回';
@override
String get map_removeLast => '移除最后一个';
@@ -2012,6 +2277,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 => '发送广播包';
@@ -2890,4 +3161,161 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get snrIndicator_lastSeen => '最近访问';
@override
String get contactsSettings_title => '联系人设置';
@override
String get contactsSettings_autoAddTitle => '自动发现';
@override
String get contactsSettings_otherTitle => '其他联系人相关设置';
@override
String get contactsSettings_autoAddUsersTitle => '自动添加用户';
@override
String get contactsSettings_autoAddUsersSubtitle => '允许伴侣自动添加发现的用户';
@override
String get contactsSettings_autoAddRepeatersTitle => '自动添加重复器';
@override
String get contactsSettings_autoAddRepeatersSubtitle => '允许伴侣自动添加发现的重复器';
@override
String get contactsSettings_autoAddRoomServersTitle => '自动添加房间服务器';
@override
String get contactsSettings_autoAddRoomServersSubtitle => '允许伴侣自动添加发现的房间服务器';
@override
String get contactsSettings_autoAddSensorsTitle => '自动添加传感器';
@override
String get contactsSettings_autoAddSensorsSubtitle => '允许伴侣自动添加发现的传感器';
@override
String get contactsSettings_overwriteOldestTitle => '覆盖最旧的';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'当联系人列表已满时,将替换最老的非收藏联系人。';
@override
String get discoveredContacts_Title => '已发现的联系人';
@override
String get discoveredContacts_noMatching => '没有匹配的联系人';
@override
String get discoveredContacts_searchHint => '搜索已发现的联系人';
@override
String get discoveredContacts_contactAdded => '联系人已添加';
@override
String get discoveredContacts_addContact => '添加联系人';
@override
String get discoveredContacts_copyContact => '复制联系人到剪贴板';
@override
String get discoveredContacts_deleteContact => '删除联系人';
@override
String get discoveredContacts_deleteContactAll => '删除所有发现的联系人';
@override
String get discoveredContacts_deleteContactAllContent => '您确定要删除所有发现的联系人吗?';
@override
String get chat_sendCooldown => '请稍等片刻后再尝试发送。';
@override
String get appSettings_jumpToOldestUnread => '跳转到最旧、未读的文章';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。';
@override
String get appSettings_languageHu => '匈牙利';
@override
String get appSettings_languageJa => '日语';
@override
String get appSettings_languageKo => '韩语';
@override
String get radioStats_tooltip => '无线电和网状结构统计数据';
@override
String get radioStats_screenTitle => '广播统计数据';
@override
String get radioStats_notConnected => '连接到设备以查看收音机统计信息。';
@override
String get radioStats_firmwareTooOld => '使用无线电统计功能需要配合使用 v8 或更高版本的固件。';
@override
String get radioStats_waiting => '正在等待数据…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return '噪声水平:$noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return '上次 RSSI 值:$rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return '上次 SNR$snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX 频道播出时间(总时长):$seconds';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX 使用时长(总时长):$seconds';
}
@override
String get radioStats_chartCaption => '近期的噪声水平(dBm)。';
@override
String radioStats_stripNoise(int noiseDbm) {
return '噪声水平:$noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => '正在获取收音机数据…';
@override
String get radioStats_settingsTile => '广播统计数据';
@override
String get radioStats_settingsSubtitle => '噪声水平、RSSI、信噪比和空中时间';
@override
String get scanner_linuxPairingShowPin => '显示 PIN码';
@override
String get scanner_linuxPairingHidePin => '隐藏 PIN';
@override
String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return '输入 $deviceName 的 PIN(如果没有,请留空)。';
}
}
+222 -2
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nieuwe Groep",
"contacts_groupName": "Groepnaam",
"contacts_groupNameRequired": "De groepnaam is verplicht.",
"contacts_groupNameReserved": "Deze groepsnaam is gereserveerd",
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1606,8 @@
"map_runTrace": "Padeshulp traceren",
"scanner_enableBluetooth": "Activeer Bluetooth",
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
"scanner_chromeRequired": "Chrome-browser vereist",
"scanner_chromeRequiredMessage": "Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.",
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
"snrIndicator_lastSeen": "Laatst gezien",
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
@@ -1799,5 +1802,222 @@
"contacts_searchContactsNoNumber": "Zoek contacten...",
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
}
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
"contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen",
"contactsSettings_title": "Instellingen voor contacten",
"settings_contactSettings": "Contactinstellingen",
"contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken",
"contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt",
"contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen",
"contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.",
"contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen",
"settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten",
"contactsSettings_autoAddTitle": "Automatische detectie",
"contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt",
"contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt",
"contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen",
"contactsSettings_overwriteOldestTitle": "Overschrijf Oudste",
"discoveredContacts_noMatching": "Geen overeenkomende contacten",
"discoveredContacts_addContact": "Contact toevoegen",
"discoveredContacts_copyContact": "Kopieer contact naar klembord",
"discoveredContacts_deleteContact": "Contact verwijderen",
"discoveredContacts_Title": "Ontdekte contacten",
"discoveredContacts_contactAdded": "Contact toegevoegd",
"discoveredContacts_searchHint": "Ontdekte contacten zoeken",
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
"common_deleteAll": "Alles verwijderen",
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
"map_guessedLocation": "Geroerde locatie",
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Verbind via USB",
"usbScreenStatus": "Selecteer een USB-apparaat",
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.",
"usbErrorPermissionDenied": "Toegang via USB is geweigerd.",
"usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.",
"usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.",
"usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.",
"usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.",
"usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.",
"usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.",
"usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.",
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
"usbFallbackDeviceName": "Web-serieapparaat",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbConnectionFailed": "Fout bij de USB-verbinding: {error}",
"usbStatus_notConnected": "Selecteer een USB-apparaat",
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
"usbStatus_searching": "Zoeken naar USB-apparaten...",
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpScreenTitle": "Verbind via TCP",
"tcpHostLabel": "IP-adres",
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Poort",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Voer het eindpunt in en verbind",
"tcpStatus_connectingTo": "Verbinding maken met {endpoint}...",
"tcpErrorHostRequired": "Een IP-adres is vereist.",
"tcpErrorPortInvalid": "De poortwaarde moet tussen 1 en 65535 liggen.",
"tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.",
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
"map_showDiscoveryContacts": "Ontdek contacten weergeven",
"map_setAsMyLocation": "Stel dit in als mijn locatie",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Privacyinstellingen",
"settings_privacySubtitle": "Beheer welke informatie wordt gedeeld",
"settings_telemetryLocationMode": "Telemetrie-locatiemodus",
"settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus",
"settings_advertLocation": "Advertentielocatie",
"settings_advertLocationSubtitle": "Locatie opnemen in advertentie",
"settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen",
"settings_allowByContact": "Toestaan op basis van contactvlaggen",
"settings_allowAll": "Alles toestaan",
"settings_denyAll": "Weiger alles",
"contact_info": "Contactinformatie",
"settings_telemetryBaseMode": "Telemetrie-basismodus",
"contact_teleBase": "Telemetrie_basis",
"contact_teleLoc": "Telemetrielocatie",
"contact_teleLocSubtitle": "Locatiegegevens delen toestaan",
"contact_teleEnv": "Telemetrieomgeving",
"contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan",
"contact_settings": "Contactinstellingen",
"contact_telemetry": "Telemetrie",
"contact_lastSeen": "Laatst gezien",
"contact_clearChat": "Chat leegmaken",
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
"@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",
"appSettings_initialRouteWeightSubtitle": "Startgewicht voor nieuwe, ontdekte routes",
"appSettings_routeWeightSuccessIncrement": "Toename in het gewicht van het succes",
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht wordt toegevoegd aan een route na een succesvolle levering.",
"appSettings_routeWeightFailureDecrement": "Vermindering van het gewicht van fouten",
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering",
"appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen",
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Herhalingssleutel overlapt",
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Ga naar het oudste ongelezen bericht",
"appSettings_jumpToOldestUnreadSubtitle": "Bij het openen van een chat met ongelezen berichten, scroll dan naar het eerste ongelezen bericht, in plaats van naar het meest recente.",
"chat_sendCooldown": "Gelieve even te wachten voordat u opnieuw verzendt.",
"appSettings_languageHu": "Hongaars",
"appSettings_languageJa": "Japanisch",
"appSettings_languageKo": "Koreaans",
"radioStats_tooltip": "Statistieken voor radio en mesh-netwerken",
"radioStats_screenTitle": "Statistieken over radio",
"radioStats_notConnected": "Verbind met een apparaat om radio-statistieken te bekijken.",
"radioStats_firmwareTooOld": "Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.",
"radioStats_waiting": "Wacht op gegevens…",
"radioStats_noiseFloor": "Ruisfrequentie: {noiseDbm} dBm",
"radioStats_lastRssi": "Laatste RSSI-waarde: {rssiDbm} dBm",
"radioStats_lastSnr": "Laatste SNR: {snr} dB",
"radioStats_txAir": "TX-tijd (totaal): {seconds} s",
"radioStats_rxAir": "Tijd besteed met RX (totaal): {seconds} s",
"radioStats_chartCaption": "Ruisfrequentie (dBm) over recente metingen.",
"radioStats_stripNoise": "Ruisfrequentie: {noiseDbm} dBm",
"radioStats_stripWaiting": "Radio-statistieken ophalen…",
"radioStats_settingsTile": "Statistieken over radio",
"radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"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"
}
+474 -216
View File
File diff suppressed because it is too large Load Diff
+222 -2
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Novo Grupo",
"contacts_groupName": "Nome do grupo",
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
"contacts_groupNameReserved": "Este nome de grupo está reservado",
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1606,6 +1607,8 @@
"scanner_enableBluetooth": "Ative o Bluetooth",
"scanner_bluetoothOff": "Bluetooth está desativado",
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
"scanner_chromeRequired": "Navegador Chrome necessário",
"scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.",
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
"snrIndicator_lastSeen": "Visto pela última vez",
"chat_ShowAllPaths": "Mostrar todos os caminhos",
@@ -1799,5 +1802,222 @@
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
"contacts_unread": "Não lido",
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
}
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
"settings_contactSettings": "Configurações de Contato",
"contactsSettings_otherTitle": "Outras configurações relacionadas a contatos",
"contactsSettings_title": "Configurações de contatos",
"contactsSettings_autoAddTitle": "Descoberta Automática",
"settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados",
"contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente",
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.",
"contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala",
"contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo",
"contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente",
"discoveredContacts_Title": "Contatos Descobertos",
"contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.",
"contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente",
"discoveredContacts_noMatching": "Nenhum contato correspondente",
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.",
"discoveredContacts_searchHint": "Pesquisar contatos descobertos",
"contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.",
"discoveredContacts_copyContact": "Copiar Contato para a área de transferência",
"discoveredContacts_deleteContact": "Excluir Contato",
"discoveredContacts_contactAdded": "Contato adicionado",
"discoveredContacts_addContact": "Adicionar Contato",
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
"common_deleteAll": "Excluir Tudo",
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
"map_guessedLocation": "Localização estimada",
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Conecte via USB",
"usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.",
"usbScreenStatus": "Selecione um dispositivo USB",
"usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.",
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.",
"usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.",
"usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.",
"usbErrorInvalidPort": "Selecione um dispositivo USB válido.",
"usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.",
"usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.",
"usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.",
"usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.",
"usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.",
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
"usbErrorPortClosed": "A conexão USB não está ativa.",
"usbFallbackDeviceName": "Dispositivo de Serial para a Web",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Procurando por dispositivos USB...",
"usbStatus_notConnected": "Selecione um dispositivo USB",
"usbConnectionFailed": "Falha na conexão USB: {error}",
"usbStatus_connecting": "Conectando ao dispositivo USB...",
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "Endereço IP",
"connectionChoiceTcpLabel": "TCP",
"tcpScreenTitle": "Estabelecer conexão via TCP",
"tcpHostHint": "192.168.40.10",
"tcpPortLabel": "Porta",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Insira o endereço final e conecte-se.",
"tcpStatus_connectingTo": "Conectando a {endpoint}...",
"tcpErrorHostRequired": "É necessário fornecer um endereço IP.",
"tcpErrorPortInvalid": "O valor do porto deve estar entre 1 e 65535.",
"tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.",
"tcpErrorTimedOut": "A conexão TCP expirou.",
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
"map_setAsMyLocation": "Defina minha localização",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.",
"settings_allowByContact": "Permitir por bandeiras de contato",
"settings_telemetryLocationMode": "Modo de Localização de Telemetria",
"settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria",
"settings_advertLocation": "Localização do Anúncio",
"settings_advertLocationSubtitle": "Incluir localização no anúncio",
"settings_privacySubtitle": "Controle o que é compartilhado.",
"settings_denyAll": "Negar todos",
"settings_allowAll": "Permitir todos",
"settings_privacy": "Configurações de Privacidade",
"contact_info": "Informações de Contato",
"settings_telemetryBaseMode": "Modo Base de Telemetria",
"contact_teleBase": "Base de Telemetria",
"contact_teleLoc": "Localização de Telemetria",
"contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização",
"contact_teleEnv": "Ambiente de Telemetria",
"contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente",
"contact_lastSeen": "Visto pela última vez",
"contact_clearChat": "Limpar Chat",
"contact_telemetry": "Telemetria",
"contact_settings": "Configurações de Contato",
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
"@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.",
"appSettings_initialRouteWeightSubtitle": "Peso inicial para novos caminhos descobertos",
"appSettings_routeWeightSuccessIncrement": "Aumento do peso para indicar sucesso",
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso adicionado a um caminho após a entrega bem-sucedida.",
"appSettings_routeWeightFailureDecrement": "Redução do peso da falha",
"appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.",
"appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens",
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sobreposições da Chave Repeater",
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Vá para a mensagem mais antiga não lida",
"chat_sendCooldown": "Por favor, aguarde um momento antes de reenviar.",
"appSettings_languageHu": "Húngaro",
"appSettings_jumpToOldestUnreadSubtitle": "Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.",
"appSettings_languageJa": "Japonês",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Estatísticas de rádio e malha",
"radioStats_screenTitle": "Estatísticas de rádio",
"radioStats_notConnected": "Conecte-se a um dispositivo para visualizar estatísticas de rádio.",
"radioStats_firmwareTooOld": "As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.",
"radioStats_waiting": "Aguardando dados…",
"radioStats_noiseFloor": "Nível de ruído: {noiseDbm} dBm",
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Último SNR: {snr} dB",
"radioStats_txAir": "Tempo de transmissão da TX (total): {seconds} s",
"radioStats_rxAir": "Tempo de uso do RX (total): {seconds} s",
"radioStats_chartCaption": "Nível de ruído (dBm) em amostras recentes.",
"radioStats_stripNoise": "Nível de ruído: {noiseDbm} dBm",
"radioStats_stripWaiting": "Obtendo estatísticas de rádio…",
"radioStats_settingsTile": "Estatísticas de rádio",
"radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"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"
}
+222 -2
View File
@@ -212,6 +212,7 @@
"contacts_newGroup": "Новая группа",
"contacts_groupName": "Имя группы",
"contacts_groupNameRequired": "Имя группы обязательно",
"contacts_groupNameReserved": "Это имя группы зарезервировано",
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
"contacts_filterContacts": "Фильтр контактов...",
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
@@ -846,6 +847,8 @@
"scanner_enableBluetooth": "Включите Bluetooth",
"scanner_bluetoothOff": "Bluetooth выключен",
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
"scanner_chromeRequired": "Требуется браузер Chrome",
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
"snrIndicator_lastSeen": "Последний раз видели",
"chat_ShowAllPaths": "Показать все пути",
@@ -1039,5 +1042,222 @@
"contacts_unread": "Непрочитанное",
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
}
"contacts_searchUsers": "Поиск {number}{str} пользователей...",
"settings_contactSettings": "Настройки контактов",
"settings_contactSettingsSubtitle": "Настройки добавления контактов",
"contactsSettings_autoAddTitle": "Автоматическое обнаружение",
"contactsSettings_title": "Настройки контактов",
"contactsSettings_otherTitle": "Другие настройки, связанные с контактами",
"contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей",
"contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат",
"contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики",
"contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики",
"contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей",
"contactsSettings_overwriteOldestTitle": "Перезаписать самое старое",
"contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы",
"contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы",
"contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.",
"discoveredContacts_noMatching": "Нет совпадающих контактов",
"discoveredContacts_searchHint": "Найденные контакты поиска",
"discoveredContacts_contactAdded": "Контакт добавлен",
"discoveredContacts_copyContact": "Копировать контакт в буфер обмена",
"discoveredContacts_addContact": "Добавить контакт",
"discoveredContacts_Title": "Обнаруженные контакты",
"discoveredContacts_deleteContact": "Удалить контакт",
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
"common_deleteAll": "Удалить все",
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
"map_guessedLocation": "Угаданное место",
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.",
"usbScreenTitle": "Подключение через USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Выберите USB-устройство",
"usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.",
"usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.",
"usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.",
"usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.",
"usbErrorInvalidPort": "Выберите действительное USB-устройство.",
"usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.",
"usbErrorNotConnected": "Ни одно USB-устройство не подключено.",
"usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.",
"usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.",
"usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.",
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
"usbErrorPortClosed": "USB-соединение не установлено.",
"usbFallbackDeviceName": "Устройство для последовательного подключения к сети",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Поиск USB-устройств...",
"usbStatus_connecting": "Подключение к USB-устройству...",
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
"usbStatus_notConnected": "Выберите USB-устройство",
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP-адрес",
"tcpScreenTitle": "Установить соединение по протоколу TCP",
"tcpPortLabel": "Порт",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Введите адрес и подключитесь.",
"tcpStatus_connectingTo": "Подключение к {endpoint}...",
"tcpErrorHostRequired": "Необходимо указать IP-адрес.",
"tcpErrorPortInvalid": "Порт должен находиться в диапазоне от 1 до 65535.",
"tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.",
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
"map_showDiscoveryContacts": "Показать контакты Discovery",
"map_setAsMyLocation": "Установить мое местоположение",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Настройки конфиденциальности",
"settings_privacySubtitle": "Контролируйте, какую информацию делиться.",
"settings_telemetryLocationMode": "Режим местоположения телеметрии",
"settings_telemetryEnvironmentMode": "Режим среды телеметрии",
"settings_advertLocation": "Местоположение рекламы",
"settings_advertLocationSubtitle": "Включить местоположение в объявление",
"settings_allowAll": "Разрешить все",
"settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.",
"settings_denyAll": "Отклонить все",
"settings_allowByContact": "Разрешить по флагам контактов",
"contact_info": "Контактная информация",
"settings_telemetryBaseMode": "Базовый режим телеметрии",
"contact_teleBase": "База телеметрии",
"contact_teleLoc": "Местоположение телеметрии",
"contact_teleLocSubtitle": "Разрешить обмен данными о местоположении",
"contact_teleEnv": "Среда телеметрии",
"contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды",
"contact_settings": "Настройки контактов",
"contact_telemetry": "Телеметрия",
"contact_clearChat": "Очистить чат",
"contact_lastSeen": "Последний раз видели",
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
"appSettings_initialRouteWeight": "Начальный вес маршрута",
"appSettings_routeWeightSuccessIncrement": "Увеличение веса успеха",
"appSettings_routeWeightSuccessIncrementSubtitle": "Вес, добавленный к маршруту после успешной доставки.",
"appSettings_routeWeightFailureDecrement": "Уменьшение веса неудачи",
"appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.",
"appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения",
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"settings_multiAck": "Мульти-ACK: {value}",
"map_showOverlaps": "Перекрытия ключа повтора",
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.",
"appSettings_jumpToOldestUnread": "Перейти к самому старому непрочитанному сообщению",
"appSettings_languageHu": "Венгерский",
"appSettings_jumpToOldestUnreadSubtitle": "При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.",
"appSettings_languageJa": "Японский",
"appSettings_languageKo": "Корейский",
"radioStats_tooltip": "Статистика радио и беспроводной сети",
"radioStats_screenTitle": "Статистика радиовещания",
"radioStats_notConnected": "Подключитесь к устройству, чтобы просмотреть статистику радио.",
"radioStats_firmwareTooOld": "Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.",
"radioStats_waiting": "Ожидаем данных…",
"radioStats_noiseFloor": "Уровень шума: {noiseDbm} дБм",
"radioStats_lastRssi": "Последнее значение RSSI: {rssiDbm} дБм",
"radioStats_lastSnr": "Последнее значение SNR: {snr} дБ",
"radioStats_txAir": "Время эфира на телеканале TX (общее): {seconds} секунд",
"radioStats_rxAir": "Общее время использования RX (в секундах): {seconds} с",
"radioStats_chartCaption": "Уровень шума (дБм) на основе последних измерений.",
"radioStats_stripNoise": "Уровень шума: {noiseDbm} дБм",
"radioStats_stripWaiting": "Получение данных о радио…",
"radioStats_settingsTile": "Статистика радиовещания",
"radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Показать PIN",
"scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
"scanner_linuxPairingHidePin": "Скрыть PIN",
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
"repeater_cliQuickDiscovery": "Обнаружить Соседей",
"repeater_cliQuickClockSync": "Синхронизация часов"
}
+222 -2
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nová skupina",
"contacts_groupName": "Názov skupiny",
"contacts_groupNameRequired": "Skupina musí mať názov.",
"contacts_groupNameReserved": "Tento názov skupiny je rezervovaný",
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1604,6 +1605,8 @@
"map_runTrace": "Spustiť trasovaním cesty",
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
"scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome",
"scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.",
"scanner_bluetoothOff": "Bluetooth je vypnutý",
"scanner_enableBluetooth": "Povolte Bluetooth",
"snrIndicator_lastSeen": "Naposledy videný",
@@ -1799,5 +1802,222 @@
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
"contacts_unread": "Neprečítané"
}
"contacts_unread": "Neprečítané",
"settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.",
"contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov",
"contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.",
"contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače",
"contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti",
"contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.",
"contactsSettings_autoAddTitle": "Automatické zisťovanie",
"contactsSettings_title": "Nastavenia kontaktov",
"contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami",
"settings_contactSettings": "Nastavenia kontaktov",
"contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory",
"discoveredContacts_noMatching": "Žiadne zhodné kontakty",
"discoveredContacts_searchHint": "Vyhľadať objavené kontakty",
"contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.",
"discoveredContacts_contactAdded": "Kontakt bol pridaný",
"discoveredContacts_copyContact": "Kopírovať kontakt do schránky",
"discoveredContacts_deleteContact": "Zmazať kontakt",
"contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.",
"discoveredContacts_Title": "Objavené kontakty",
"contactsSettings_overwriteOldestTitle": "Prepísať najstaršie",
"discoveredContacts_addContact": "Pridať kontakt",
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
"common_deleteAll": "Zmazať všetko",
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
"map_guessedLocation": "Odhadnutá lokalita",
"usbScreenTitle": "Pripojte cez USB",
"usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Vyberte USB zariadenie",
"usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.",
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.",
"usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.",
"usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.",
"usbErrorInvalidPort": "Vyberte platné USB zariadenie.",
"usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.",
"usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.",
"usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.",
"usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.",
"usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.",
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
"usbFallbackDeviceName": "Webový sériový zariadenie",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Hľadanie USB zariadení...",
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
"usbStatus_notConnected": "Vyberte USB zariadenie",
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP adresa",
"tcpScreenTitle": "Spojte sa pomocou protokolu TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.",
"tcpStatus_connectingTo": "Pripojenie k {endpoint}...",
"tcpErrorHostRequired": "Je potrebné zadať IP adresu.",
"tcpErrorPortInvalid": "Číslo portu musí byť medzi 1 a 65535.",
"tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.",
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
"map_showDiscoveryContacts": "Zobraziť kontakty objavov",
"map_setAsMyLocation": "Nastavte ako moju polohu",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Nastavenia súkromia",
"settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.",
"settings_telemetryLocationMode": "Režim umiestnenia telemetrie",
"settings_telemetryBaseMode": "Základný režim telemetrie",
"settings_advertLocation": "Umiestnenie inzerátu",
"settings_telemetryEnvironmentMode": "Režim prostredia telemetrie",
"settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu",
"settings_allowAll": "Povoliť všetko",
"settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.",
"settings_denyAll": "Zamietnuť všetko",
"settings_allowByContact": "Povoliť podľa kontaktových vlajok",
"contact_info": "Kontaktné informácie",
"contact_settings": "Nastavenia kontaktov",
"contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie",
"contact_teleLoc": "Lokácia telemetrie",
"contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite",
"contact_teleEnv": "Prostredie telemetrie",
"contact_telemetry": "Telemetria",
"contact_clearChat": "Vymazať chat",
"contact_lastSeen": "Naposledy videný",
"contact_teleBase": "Báza telemetrie",
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
"@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",
"appSettings_maxRouteWeight": "Maximálna hmotnosť trasy",
"appSettings_routeWeightSuccessIncrement": "Zvyšenie váhy úspechu",
"appSettings_routeWeightSuccessIncrementSubtitle": "Hmotnosť pridaná k trase po úspešnej doručení",
"appSettings_routeWeightFailureDecrement": "Sníženie váhy, ktorá sa používa na odhad rizika.",
"appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie",
"appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ",
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
"settings_multiAck": "Viaceré ACK: {value}",
"map_showOverlaps": "Prekrývanie opakovača kľúča",
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Prosím, počkajte chvíľu, než zašlete znova.",
"appSettings_jumpToOldestUnread": "Presk oceň",
"appSettings_jumpToOldestUnreadSubtitle": "Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.",
"appSettings_languageHu": "Maďarský",
"appSettings_languageJa": "Japonský",
"appSettings_languageKo": "Kórejský",
"radioStats_tooltip": "Statistiky rádiových a sieťových kanálov",
"radioStats_screenTitle": "Štatistiky rádiových vysielaní",
"radioStats_notConnected": "Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.",
"radioStats_firmwareTooOld": "Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.",
"radioStats_waiting": "Čakám na údaje…",
"radioStats_noiseFloor": "Úroveň hluku: {noiseDbm} dBm",
"radioStats_lastRssi": "Posledný údaj RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Posledná hodnota SNR: {snr} dB",
"radioStats_txAir": "Čas vysielania na TX (celkový): {seconds} s",
"radioStats_rxAir": "Čas RX (celkový): {seconds} s",
"radioStats_chartCaption": "Úroveň šumu (dBm) pre posledné vzorky.",
"radioStats_stripNoise": "Úroveň hluku: {noiseDbm} dBm",
"radioStats_stripWaiting": "Získavanie údajov o rádiu…",
"radioStats_settingsTile": "Štatistiky rádiových vysielaní",
"radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak nie je, nechajte prázdne).",
"scanner_linuxPairingShowPin": "Zobraziť PIN",
"scanner_linuxPairingHidePin": "Skryť PIN",
"scanner_linuxPairingPinTitle": "Bluetooth párovací PIN",
"repeater_cliQuickClockSync": "Synchronizácia hodin",
"repeater_cliQuickDiscovery": "Objaviť susedov"
}
+222 -2
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nova skupina",
"contacts_groupName": "Ime skupine",
"contacts_groupNameRequired": "Ime skupine je obvezno.",
"contacts_groupNameReserved": "To ime skupine je rezervirano",
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1606,8 @@
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
"scanner_enableBluetooth": "Omogočite Bluetooth",
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
"scanner_chromeRequired": "Zahtevan brskalnik Chrome",
"scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.",
"scanner_bluetoothOff": "Bluetooth je izklopljen",
"snrIndicator_lastSeen": "Zadnjič videno",
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
@@ -1799,5 +1802,222 @@
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
"contacts_searchContactsNoNumber": "Iskanje stikov...",
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
}
"contacts_searchUsers": "Išči {number}{str} uporabnikov...",
"settings_contactSettings": "Nastavitve stika",
"contactsSettings_autoAddTitle": "Avtomatsko odkrivanje",
"contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike",
"contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje",
"contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.",
"contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob",
"contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.",
"contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki",
"settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.",
"contactsSettings_title": "Nastavitve stikov",
"contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje",
"contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.",
"discoveredContacts_noMatching": "Ni ujemajočih stikov",
"contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.",
"discoveredContacts_addContact": "Dodaj stik",
"discoveredContacts_contactAdded": "Kontakt dodan",
"discoveredContacts_copyContact": "Kopiraj stik v odložišče",
"contactsSettings_overwriteOldestTitle": "Prepiši najstarejše",
"discoveredContacts_Title": "Odkriti stiki",
"discoveredContacts_searchHint": "Najdeni stiki po iskanju",
"discoveredContacts_deleteContact": "Izbriši stik",
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
"common_deleteAll": "Izbriši vse",
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
"map_guessedLocation": "Predpostavljena lokacija",
"map_showGuessedLocations": "Pokaži lokacije domnevnih not.",
"usbScreenTitle": "Povežite preko USB",
"usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenStatus": "Izberite USB naprave",
"usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.",
"usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.",
"usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.",
"usbErrorDeviceMissing": "Izbrani USB napravej je več ne.",
"usbErrorInvalidPort": "Izberite veljavno USB naprave.",
"usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.",
"usbErrorNotConnected": "Ni priklopljenih USB naprave.",
"usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.",
"usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.",
"usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.",
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
"usbErrorPortClosed": "USB povezava ni aktivirana.",
"usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_notConnected": "Izberite USB naprave.",
"usbStatus_connecting": "Povezava z USB napravo...",
"usbStatus_searching": "Iskanje USB naprav...",
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"connectionChoiceTcpLabel": "TCP",
"tcpHostLabel": "IP naslov",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Komunicirajte preko protokola TCP",
"tcpPortLabel": "Vrata",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Vnesite končni naslov in se povežite",
"tcpStatus_connectingTo": "Povezava z {endpoint}...",
"tcpErrorHostRequired": "Potrebna je IP-naslov.",
"tcpErrorPortInvalid": "Port mora biti med 1 in 65535.",
"tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.",
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
"map_setAsMyLocation": "Nastavite to kot mojo lokacijo",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Nastavitve zasebnosti",
"settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.",
"settings_telemetryBaseMode": "Osnovni način telemetrije",
"settings_telemetryLocationMode": "Način delovanja telemetrije",
"settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije",
"settings_advertLocation": "Lokacija oglasa",
"settings_allowByContact": "Dovoli po kontaktnih zastavah",
"settings_denyAll": "Zavrniti vse",
"settings_allowAll": "Dovoli vse",
"settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.",
"contact_info": "Kontaktni podatki",
"contact_teleBase": "Baza telemetrije",
"contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije",
"contact_teleLoc": "Lokacija telemetrije",
"contact_lastSeen": "Zadnjič videno",
"contact_settings": "Nastavitve stika",
"settings_advertLocationSubtitle": "Vključi lokacijo v oglas.",
"contact_telemetry": "Telemetrija",
"contact_clearChat": "Počisti klepet",
"contact_teleEnv": "Okolje telemetrije",
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
"@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",
"appSettings_maxRouteWeight": "Največja dovoljena teža poti",
"appSettings_routeWeightSuccessIncrement": "Učinkovitost: povečanje",
"appSettings_routeWeightSuccessIncrementSubtitle": "Težava, dodana poti po uspešni dostavi",
"appSettings_routeWeightFailureDecrement": "Zmanjšanje teže, ki je povezana s pomanjkanjem",
"appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.",
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
"path_routeWeight": "{weight}/{max}",
"settings_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.",
"@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",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"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"
}
+222 -2
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Ny grupp",
"contacts_groupName": "Gruppnamn",
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
"contacts_groupNameReserved": "Detta gruppnamn är reserverat",
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1606,8 @@
"map_removeLast": "Ta bort sista",
"scanner_enableBluetooth": "Aktivera Bluetooth",
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
"scanner_chromeRequired": "Chrome-webbläsare krävs",
"scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.",
"scanner_bluetoothOff": "Bluetooth är avstängt",
"snrIndicator_lastSeen": "Senast sedd",
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
@@ -1799,5 +1802,222 @@
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
"contacts_searchUsers": "Sök {number}{str} användare...",
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
}
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
"settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.",
"settings_contactSettings": "Kontaktinställningar",
"contactsSettings_autoAddTitle": "Automatisk upptäckt",
"contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt",
"contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare",
"contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt",
"contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.",
"contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt",
"contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt",
"contactsSettings_title": "Kontaktinställningar",
"contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.",
"contactsSettings_overwriteOldestTitle": "Skriv över äldst",
"contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.",
"contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar",
"discoveredContacts_noMatching": "Inga matchande kontakter",
"discoveredContacts_searchHint": "Sök uppfunna kontakter",
"discoveredContacts_deleteContact": "Ta bort kontakt",
"discoveredContacts_Title": "Upptäckta kontakter",
"discoveredContacts_contactAdded": "Kontakt tillagd",
"discoveredContacts_addContact": "Lägg till kontakt",
"discoveredContacts_copyContact": "Kopiera kontakt till urklipp",
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
"common_deleteAll": "Ta bort alla",
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
"map_guessedLocation": "Gissad plats",
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
"usbScreenTitle": "Anslut via USB",
"usbScreenStatus": "Välj en USB-enhet",
"usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.",
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
"usbErrorInvalidPort": "Välj en giltig USB-enhet.",
"usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.",
"usbErrorNotConnected": "Ingen USB-enhet är ansluten.",
"usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.",
"usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.",
"usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.",
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
"usbFallbackDeviceName": "Web-serieenhet",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_connecting": "Anslutning till USB-enhet...",
"usbStatus_notConnected": "Välj en USB-enhet",
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
"usbStatus_searching": "Söker efter USB-enheter...",
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP-adress",
"tcpScreenTitle": "Anslut via TCP",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Ange slutpunkt och anslut",
"tcpStatus_connectingTo": "Anslutning till {endpoint}...",
"tcpErrorHostRequired": "IP-adress krävs.",
"tcpErrorPortInvalid": "Porten måste vara mellan 1 och 65535.",
"tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.",
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
"map_showDiscoveryContacts": "Visa Discovery-kontakter",
"map_setAsMyLocation": "Ange som min plats",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacy": "Inställningar för sekretess",
"settings_allowAll": "Tillåt alla",
"settings_privacySubtitle": "Kontrollera vilken information som delas.",
"settings_telemetryEnvironmentMode": "Telemetri miljöläge",
"settings_telemetryBaseMode": "Telemetribasläge",
"settings_telemetryLocationMode": "Telemetritillstånd för plats",
"settings_advertLocation": "Annonsplacering",
"contact_info": "Kontaktinformation",
"contact_settings": "Kontaktinställningar",
"contact_telemetry": "Telemetri",
"settings_denyAll": "Neka alla",
"settings_allowByContact": "Tillåt via kontaktflaggor",
"settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.",
"contact_lastSeen": "Senast sedd",
"contact_clearChat": "Rensa Chatt",
"contact_teleEnv": "Telemetri Miljö",
"settings_advertLocationSubtitle": "Inkludera plats i annonsen",
"contact_teleEnvSubtitle": "Tillåt delning av miljösensordata",
"contact_teleBase": "Telemetribas",
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
"contact_teleLoc": "Telemetridata plats",
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
"@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.",
"appSettings_initialRouteWeight": "Initial vikt för rutt",
"appSettings_routeWeightSuccessIncrement": "Ökning av vikt för framgång",
"appSettings_routeWeightSuccessIncrementSubtitle": "Vikt läggs till en väg efter en lyckad leverans.",
"appSettings_routeWeightFailureDecrement": "Minskning av vikten för misslyckande",
"appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök",
"appSettings_maxMessageRetries": "Maximalt antal försök",
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Repeater-nyckelöverlappningar",
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnreadSubtitle": "När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.",
"chat_sendCooldown": "Vänligen vänta en stund innan du skickar igen.",
"appSettings_jumpToOldestUnread": "Gå direkt till det äldsta, obesvarade meddelandet",
"appSettings_languageHu": "Ungerskt",
"appSettings_languageJa": "Japanska",
"appSettings_languageKo": "Koreanska",
"radioStats_tooltip": "Radio- och mesh-statistik",
"radioStats_screenTitle": "Radiostation",
"radioStats_notConnected": "Anslut till en enhet för att visa radiostatistik.",
"radioStats_firmwareTooOld": "Radio statistik kräver kompatibel firmware version 8 eller senare.",
"radioStats_waiting": "Väntar på data…",
"radioStats_noiseFloor": "Bakgrundsnivå: {noiseDbm} dBm",
"radioStats_lastRssi": "Senaste RSSI-värde: {rssiDbm} dBm",
"radioStats_lastSnr": "Senaste SNR: {snr} dB",
"radioStats_txAir": "TX-tid (total): {seconds} sekunder",
"radioStats_rxAir": "RX-tid (total): {seconds} s",
"radioStats_chartCaption": "Ljudnivå (dBm) baserat på de senaste mätningarna.",
"radioStats_stripNoise": "Bakgrundsnivå: {noiseDbm} dBm",
"radioStats_stripWaiting": "Hämtar radiostatistik…",
"radioStats_settingsTile": "Radiostation",
"radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"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"
}
+222 -2
View File
@@ -286,6 +286,7 @@
"contacts_newGroup": "Нова група",
"contacts_groupName": "Назва групи",
"contacts_groupNameRequired": "Назва групи обов'язкова.",
"contacts_groupNameReserved": "Ця назва групи зарезервована",
"contacts_groupAlreadyExists": "Група «{name}» вже існує.",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1605,6 +1606,8 @@
"map_pathTraceCancelled": "Відмінується трасування шляху",
"scanner_enableBluetooth": "Увімкніть Bluetooth",
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
"scanner_chromeRequired": "Потрібен браузер Chrome",
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
"scanner_bluetoothOff": "Bluetooth вимкнено",
"snrIndicator_lastSeen": "Останній раз бачили",
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
@@ -1799,5 +1802,222 @@
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
"contacts_searchContactsNoNumber": "Пошук контактів...",
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
"contacts_unread": "Непрочитане"
}
"contacts_unread": "Непрочитане",
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
"settings_contactSettings": "Налаштування контактів",
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
"contactsSettings_title": "Налаштування контактів",
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
"discoveredContacts_searchHint": "Знайти виявлені контакти",
"discoveredContacts_contactAdded": "Контакт додано",
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
"discoveredContacts_Title": "Виявлені контакти",
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
"discoveredContacts_deleteContact": "Видалити контакт",
"discoveredContacts_copyContact": "Копіювати контакт у буфер обміну",
"discoveredContacts_addContact": "Додати контакт",
"contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.",
"common_deleteAll": "Видалити все",
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
"map_guessedLocation": "Визначено місцезнаходження",
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
"usbScreenTitle": "Підключити через USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB",
"usbScreenStatus": "Виберіть пристрій USB",
"usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.",
"usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.",
"usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.",
"usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.",
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
"usbErrorPortClosed": "З'єднання USB не встановлено.",
"usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Пошук пристроїв USB...",
"usbStatus_notConnected": "Виберіть пристрій USB",
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
"usbStatus_connecting": "Підключення до USB-пристрою...",
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpHostLabel": "IP-адреса",
"tcpScreenTitle": "З'єднатися через протокол TCP",
"tcpPortLabel": "Порт",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Введіть кінцеву точку та підключіться",
"tcpStatus_connectingTo": "Підключення до {endpoint}...",
"tcpErrorHostRequired": "Необхідно вказати IP-адресу.",
"tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.",
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
"map_showDiscoveryContacts": "Показати контакти Відкриття",
"map_setAsMyLocation": "Встановити моє місцезнаходження",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
"settings_privacy": "Налаштування приватності",
"settings_telemetryBaseMode": "Режим базової телеметрії",
"settings_telemetryLocationMode": "Режим місця телеметрії",
"settings_advertLocation": "Розміщення реклами",
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
"settings_allowAll": "Дозволити все",
"settings_denyAll": "Відхилити все",
"settings_allowByContact": "Дозволити за контактними прапорцями",
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
"contact_info": "Контактна інформація",
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
"contact_teleLoc": "Розташування телеметрії",
"contact_teleBase": "Базовий телебачення",
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
"contact_settings": "Налаштування контактів",
"contact_telemetry": "Телеметрія",
"contact_clearChat": "Очистити чат",
"contact_lastSeen": "Останній раз бачили",
"contact_teleEnv": "Середовище телеметрії",
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_initialRouteWeight": "Початкова вартість маршруту",
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
"appSettings_maxRouteWeight": "Максимальна вага маршруту",
"appSettings_maxRouteWeightSubtitle": "Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.",
"appSettings_routeWeightSuccessIncrement": "Збільшення ваги успіху",
"appSettings_routeWeightSuccessIncrementSubtitle": "Вага, додана до маршруту після успішної доставки",
"appSettings_routeWeightFailureDecrement": "Зменшення ваги помилки",
"appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки",
"appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення",
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"settings_multiAck": "Багатократне підтвердження: {value}",
"map_showOverlaps": "Перекриття ключа повторювача",
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.",
"appSettings_languageHu": "Угорський",
"appSettings_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 та час, протягом якого пристрій використовує радіоканал.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
"scanner_linuxPairingShowPin": "Показати PIN",
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN",
"repeater_cliQuickClockSync": "Синхронізація годинника",
"repeater_cliQuickDiscovery": "Відкрити сусідів"
}
+222 -2
View File
@@ -300,6 +300,7 @@
"contacts_newGroup": "新建群聊",
"contacts_groupName": "群聊名称",
"contacts_groupNameRequired": "请输入群聊名称",
"contacts_groupNameReserved": "该群组名称已被保留",
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
"@contacts_groupAlreadyExists": {
"placeholders": {
@@ -1609,6 +1610,8 @@
"map_removeLast": "移除最后一个",
"map_runTrace": "运行路径追踪",
"scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备",
"scanner_chromeRequired": "需要 Chrome 浏览器",
"scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。",
"scanner_bluetoothOff": "蓝牙已关闭",
"scanner_enableBluetooth": "启用蓝牙",
"snrIndicator_lastSeen": "最近访问",
@@ -1804,5 +1807,222 @@
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
"contacts_searchContactsNoNumber": "搜索联系人...",
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
"contacts_searchFavorites": "搜索 {number}{str} 收藏..."
}
"contacts_searchFavorites": "搜索 {number}{str} 收藏...",
"settings_contactSettings": "联系人设置",
"contactsSettings_title": "联系人设置",
"contactsSettings_autoAddUsersTitle": "自动添加用户",
"contactsSettings_otherTitle": "其他联系人相关设置",
"contactsSettings_autoAddUsersSubtitle": "允许伴侣自动添加发现的用户",
"contactsSettings_autoAddRepeatersSubtitle": "允许伴侣自动添加发现的重复器",
"contactsSettings_autoAddSensorsTitle": "自动添加传感器",
"contactsSettings_autoAddRoomServersSubtitle": "允许伴侣自动添加发现的房间服务器",
"contactsSettings_autoAddRepeatersTitle": "自动添加重复器",
"contactsSettings_autoAddTitle": "自动发现",
"settings_contactSettingsSubtitle": "添加联系人的设置",
"contactsSettings_overwriteOldestTitle": "覆盖最旧的",
"contactsSettings_autoAddSensorsSubtitle": "允许伴侣自动添加发现的传感器",
"discoveredContacts_searchHint": "搜索已发现的联系人",
"contactsSettings_autoAddRoomServersTitle": "自动添加房间服务器",
"discoveredContacts_contactAdded": "联系人已添加",
"discoveredContacts_deleteContact": "删除联系人",
"discoveredContacts_addContact": "添加联系人",
"discoveredContacts_noMatching": "没有匹配的联系人",
"discoveredContacts_Title": "已发现的联系人",
"discoveredContacts_copyContact": "复制联系人到剪贴板",
"contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。",
"common_deleteAll": "删除全部",
"discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?",
"discoveredContacts_deleteContactAll": "删除所有发现的联系人",
"map_showGuessedLocations": "显示猜测的节点位置",
"map_guessedLocation": "猜测的位置",
"connectionChoiceUsbLabel": "USB",
"usbScreenTitle": "通过USB连接",
"usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。",
"connectionChoiceBluetoothLabel": "蓝牙",
"usbScreenStatus": "选择一个 USB 设备",
"usbScreenNote": "USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。",
"usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。",
"usbErrorPermissionDenied": "拒绝了USB权限。",
"usbErrorDeviceMissing": "所选的USB设备已不再可用。",
"usbErrorInvalidPort": "选择一个有效的USB设备。",
"usbErrorBusy": "还有一个 USB 连接请求正在进行中。",
"usbErrorNotConnected": "没有连接任何USB设备。",
"usbErrorOpenFailed": "未能打开所选的USB设备。",
"usbErrorConnectFailed": "未能连接到所选的USB设备。",
"usbErrorUnsupported": "此平台不支持USB串行通信。",
"usbErrorAlreadyActive": "USB 连接已建立。",
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
"usbErrorPortClosed": "USB 连接未建立。",
"usbFallbackDeviceName": "Web 串流设备",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "正在搜索 USB 设备...",
"usbStatus_connecting": "连接USB设备...",
"usbStatus_notConnected": "选择一个 USB 设备",
"usbConnectionFailed": "USB 连接失败:{error}",
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"tcpHostLabel": "IP地址",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "通过 TCP 连接",
"connectionChoiceTcpLabel": "TCP",
"tcpPortLabel": "端口",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "输入目标地址,然后连接",
"tcpStatus_connectingTo": "连接到 {endpoint}...",
"tcpErrorHostRequired": "需要提供IP地址。",
"tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。",
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
"tcpErrorTimedOut": "TCP 连接超时。",
"tcpConnectionFailed": "TCP 连接失败:{error}",
"map_showDiscoveryContacts": "显示发现联系人",
"map_setAsMyLocation": "设置为我的位置",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"settings_privacySubtitle": "控制要共享的信息。",
"settings_privacySettingsDescription": "选择您的设备与他人共享的信息。",
"settings_telemetryBaseMode": "遥测基础模式",
"settings_telemetryLocationMode": "遥测位置模式",
"settings_advertLocation": "广告位置",
"settings_advertLocationSubtitle": "在广告中包含位置",
"settings_allowByContact": "按联系人标志允许",
"settings_denyAll": "拒绝所有",
"settings_privacy": "隐私设置",
"settings_allowAll": "允许全部",
"contact_info": "联系信息",
"contact_teleBase": "遥测基站",
"contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据",
"settings_telemetryEnvironmentMode": "遥测环境模式",
"contact_teleLoc": "遥测位置",
"contact_teleEnv": "遥测环境",
"contact_teleEnvSubtitle": "允许共享环境传感器数据",
"contact_clearChat": "清除聊天记录",
"contact_lastSeen": "最近出现",
"contact_settings": "联系人设置",
"contact_teleLocSubtitle": "允许共享位置数据",
"contact_telemetry": "遥测数据",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"appSettings_maxRouteWeight": "最大路径重量",
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
"appSettings_initialRouteWeight": "初始路线权重",
"appSettings_maxRouteWeightSubtitle": "一条路径可以累积的最大重量,取决于成功交付的数量。",
"appSettings_routeWeightSuccessIncrement": "成功权重增加",
"appSettings_routeWeightSuccessIncrementSubtitle": "在成功交付后,将重量添加到路径中",
"appSettings_routeWeightFailureDecrement": "失败权重降低",
"appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。",
"appSettings_maxMessageRetries": "最大消息重试次数",
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "多重ACK{value}",
"settings_telemetryModeUpdated": "遥测模式已更新",
"map_showOverlaps": "重复键重叠",
"map_runTraceWithReturnPath": "沿着相同的路径返回",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "请稍等片刻后再尝试发送。",
"appSettings_jumpToOldestUnreadSubtitle": "在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。",
"appSettings_jumpToOldestUnread": "跳转到最旧、未读的文章",
"appSettings_languageHu": "匈牙利",
"appSettings_languageJa": "日语",
"appSettings_languageKo": "韩语",
"radioStats_tooltip": "无线电和网状结构统计数据",
"radioStats_screenTitle": "广播统计数据",
"radioStats_notConnected": "连接到设备以查看收音机统计信息。",
"radioStats_firmwareTooOld": "使用无线电统计功能需要配合使用 v8 或更高版本的固件。",
"radioStats_waiting": "正在等待数据…",
"radioStats_noiseFloor": "噪声水平:{noiseDbm} dBm",
"radioStats_lastRssi": "上次 RSSI 值:{rssiDbm} dBm",
"radioStats_lastSnr": "上次 SNR{snr} dB",
"radioStats_txAir": "TX 频道播出时间(总时长):{seconds} 秒",
"radioStats_rxAir": "RX 使用时长(总时长):{seconds} 秒",
"radioStats_chartCaption": "近期的噪声水平(dBm)。",
"radioStats_stripNoise": "噪声水平:{noiseDbm} dBm",
"radioStats_stripWaiting": "正在获取收音机数据…",
"radioStats_settingsTile": "广播统计数据",
"radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "显示 PIN码",
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN(如果没有,请留空)。",
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
"scanner_linuxPairingHidePin": "隐藏 PIN",
"repeater_cliQuickDiscovery": "发现邻居",
"repeater_cliQuickClockSync": "同步时钟"
}
+21 -1
View File
@@ -4,6 +4,9 @@ import 'package:flutter/foundation.dart';
import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'screens/chrome_required_screen.dart';
import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart';
import 'screens/scanner_screen.dart';
import 'services/storage_service.dart';
@@ -16,6 +19,8 @@ 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/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart';
import 'utils/app_logger.dart';
@@ -36,6 +41,8 @@ void main() async {
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage);
// Load settings
await appSettingsService.loadSettings();
@@ -53,6 +60,8 @@ void main() async {
_registerThirdPartyLicenses();
await chatTextScaleService.initialize();
await uiViewStateService.initialize();
await timeoutPredictionService.initialize();
// Wire up connector with services
connector.initialize(
@@ -62,6 +71,7 @@ void main() async {
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
backgroundService: backgroundService,
timeoutPredictionService: timeoutPredictionService,
);
await connector.loadContactCache();
@@ -83,6 +93,8 @@ void main() async {
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService,
),
);
}
@@ -118,6 +130,8 @@ class MeshCoreApp extends StatelessWidget {
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService;
const MeshCoreApp({
super.key,
@@ -130,6 +144,8 @@ class MeshCoreApp extends StatelessWidget {
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
required this.uiViewStateService,
required this.timeoutPredictionService,
});
@override
@@ -143,8 +159,10 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
ChangeNotifierProvider.value(value: uiViewStateService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),
ChangeNotifierProvider.value(value: timeoutPredictionService),
],
child: Consumer<AppSettingsService>(
builder: (context, settingsService, child) {
@@ -187,7 +205,9 @@ class MeshCoreApp extends StatelessWidget {
NotificationService().setLocale(locale);
return child ?? const SizedBox.shrink();
},
home: const ScannerScreen(),
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
: const ScannerScreen(),
);
},
),
+75
View File
@@ -18,10 +18,12 @@ class AppSettings {
final bool mapShowRepeaters;
final bool mapShowChatNodes;
final bool mapShowOtherNodes;
final bool mapShowOverlaps;
final double mapTimeFilterHours; // 0 = all time
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
final bool mapShowMarkers;
final bool mapShowGuessedLocations;
final bool enableMessageTracing;
final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom;
@@ -31,6 +33,11 @@ class AppSettings {
final bool notifyOnNewChannelMessage;
final bool notifyOnNewAdvert;
final bool autoRouteRotationEnabled;
final double maxRouteWeight;
final double initialRouteWeight;
final double routeWeightSuccessIncrement;
final double routeWeightFailureDecrement;
final int maxMessageRetries;
final String themeMode;
final String? languageOverride; // null = system default
final bool appDebugLogEnabled;
@@ -38,16 +45,22 @@ class AppSettings {
final Map<String, String> batteryChemistryByRepeaterId;
final UnitSystem unitSystem;
final Set<String> mutedChannels;
final bool mapShowDiscoveryContacts;
final String tcpServerAddress;
final int tcpServerPort;
final bool jumpToOldestUnread;
AppSettings({
this.clearPathOnMaxRetry = false,
this.mapShowRepeaters = true,
this.mapShowChatNodes = true,
this.mapShowOtherNodes = true,
this.mapShowOverlaps = false,
this.mapTimeFilterHours = 0, // Default to all time
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
this.mapShowMarkers = true,
this.mapShowGuessedLocations = true,
this.enableMessageTracing = false,
this.mapCacheBounds,
this.mapCacheMinZoom = 10,
@@ -57,6 +70,11 @@ class AppSettings {
this.notifyOnNewChannelMessage = true,
this.notifyOnNewAdvert = true,
this.autoRouteRotationEnabled = false,
this.maxRouteWeight = 5.0,
this.initialRouteWeight = 3.0,
this.routeWeightSuccessIncrement = 0.5,
this.routeWeightFailureDecrement = 0.2,
this.maxMessageRetries = 5,
this.themeMode = 'system',
this.languageOverride,
this.appDebugLogEnabled = false,
@@ -64,6 +82,10 @@ class AppSettings {
Map<String, String>? batteryChemistryByRepeaterId,
this.unitSystem = UnitSystem.metric,
Set<String>? mutedChannels,
this.mapShowDiscoveryContacts = true,
this.tcpServerAddress = '',
this.tcpServerPort = 0,
this.jumpToOldestUnread = false,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {};
@@ -74,10 +96,12 @@ class AppSettings {
'map_show_repeaters': mapShowRepeaters,
'map_show_chat_nodes': mapShowChatNodes,
'map_show_other_nodes': mapShowOtherNodes,
'map_show_overlaps': mapShowOverlaps,
'map_time_filter_hours': mapTimeFilterHours,
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
'map_show_markers': mapShowMarkers,
'map_show_guessed_locations': mapShowGuessedLocations,
'enable_message_tracing': enableMessageTracing,
'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom,
@@ -87,6 +111,11 @@ class AppSettings {
'notify_on_new_channel_message': notifyOnNewChannelMessage,
'notify_on_new_advert': notifyOnNewAdvert,
'auto_route_rotation_enabled': autoRouteRotationEnabled,
'max_route_weight': maxRouteWeight,
'initial_route_weight': initialRouteWeight,
'route_weight_success_increment': routeWeightSuccessIncrement,
'route_weight_failure_decrement': routeWeightFailureDecrement,
'max_message_retries': maxMessageRetries,
'theme_mode': themeMode,
'language_override': languageOverride,
'app_debug_log_enabled': appDebugLogEnabled,
@@ -94,6 +123,10 @@ class AppSettings {
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
'unit_system': unitSystem.value,
'muted_channels': mutedChannels.toList(),
'map_show_discovery_contacts': mapShowDiscoveryContacts,
'tcp_server_address': tcpServerAddress,
'tcp_server_port': tcpServerPort,
'jump_to_oldest_unread': jumpToOldestUnread,
};
}
@@ -110,11 +143,14 @@ class AppSettings {
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
mapTimeFilterHours:
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
mapShowGuessedLocations:
json['map_show_guessed_locations'] as bool? ?? true,
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
@@ -128,6 +164,14 @@ class AppSettings {
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
autoRouteRotationEnabled:
json['auto_route_rotation_enabled'] as bool? ?? false,
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
initialRouteWeight:
(json['initial_route_weight'] as num?)?.toDouble() ?? 3.0,
routeWeightSuccessIncrement:
(json['route_weight_success_increment'] as num?)?.toDouble() ?? 0.5,
routeWeightFailureDecrement:
(json['route_weight_failure_decrement'] as num?)?.toDouble() ?? 0.2,
maxMessageRetries: json['max_message_retries'] as int? ?? 5,
themeMode: json['theme_mode'] as String? ?? 'system',
languageOverride: json['language_override'] as String?,
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
@@ -147,6 +191,11 @@ class AppSettings {
?.map((e) => e.toString())
.toSet()) ??
{},
mapShowDiscoveryContacts:
json['map_show_discovery_contacts'] as bool? ?? true,
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
);
}
@@ -155,10 +204,12 @@ class AppSettings {
bool? mapShowRepeaters,
bool? mapShowChatNodes,
bool? mapShowOtherNodes,
bool? mapShowOverlaps,
double? mapTimeFilterHours,
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
bool? mapShowMarkers,
bool? mapShowGuessedLocations,
bool? enableMessageTracing,
Object? mapCacheBounds = _unset,
int? mapCacheMinZoom,
@@ -168,6 +219,11 @@ class AppSettings {
bool? notifyOnNewChannelMessage,
bool? notifyOnNewAdvert,
bool? autoRouteRotationEnabled,
double? maxRouteWeight,
double? initialRouteWeight,
double? routeWeightSuccessIncrement,
double? routeWeightFailureDecrement,
int? maxMessageRetries,
String? themeMode,
Object? languageOverride = _unset,
bool? appDebugLogEnabled,
@@ -175,16 +231,23 @@ class AppSettings {
Map<String, String>? batteryChemistryByRepeaterId,
UnitSystem? unitSystem,
Set<String>? mutedChannels,
bool? mapShowDiscoveryContacts,
String? tcpServerAddress,
int? tcpServerPort,
bool? jumpToOldestUnread,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
mapShowGuessedLocations:
mapShowGuessedLocations ?? this.mapShowGuessedLocations,
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds
@@ -198,6 +261,13 @@ class AppSettings {
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoRouteRotationEnabled:
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
initialRouteWeight: initialRouteWeight ?? this.initialRouteWeight,
routeWeightSuccessIncrement:
routeWeightSuccessIncrement ?? this.routeWeightSuccessIncrement,
routeWeightFailureDecrement:
routeWeightFailureDecrement ?? this.routeWeightFailureDecrement,
maxMessageRetries: maxMessageRetries ?? this.maxMessageRetries,
themeMode: themeMode ?? this.themeMode,
languageOverride: languageOverride == _unset
? this.languageOverride
@@ -209,6 +279,11 @@ class AppSettings {
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
unitSystem: unitSystem ?? this.unitSystem,
mutedChannels: mutedChannels ?? this.mutedChannels,
mapShowDiscoveryContacts:
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
);
}
}
+12 -9
View File
@@ -24,20 +24,23 @@ class Channel {
bool get isPublicChannel => pskHex == publicChannelPsk;
static Channel? fromFrame(Uint8List data) {
static Channel? fromFrame(Uint8List frame) {
// CHANNEL_INFO format:
// [0] = RESP_CODE_CHANNEL_INFO (18)
// [1] = channel_idx
// [2-33] = name (32 bytes, null-terminated)
// [34-49] = psk (16 bytes)
if (data.length < 50) return null;
if (data[0] != respCodeChannelInfo) return null;
final index = data[1];
final name = readCString(data, 2, 32);
final psk = Uint8List.fromList(data.sublist(34, 50));
return Channel(index: index, name: name, psk: psk);
if (frame.length < 50) return null;
final reader = BufferReader(frame);
try {
if (reader.readByte() != respCodeChannelInfo) return null;
final index = reader.readByte();
final name = reader.readCStringGreedy(32);
final psk = reader.readBytes(16);
return Channel(index: index, name: name, psk: psk);
} catch (e) {
return null;
}
}
static Channel empty(int index) {
+75 -77
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 '../utils/app_logger.dart';
enum ChannelMessageStatus { pending, sent, failed }
@@ -36,6 +37,7 @@ class ChannelMessage {
final List<Uint8List> pathVariants;
final int? channelIndex;
final String messageId;
final String? packetHash;
final String? replyToMessageId;
final String? replyToSenderName;
final String? replyToText;
@@ -55,6 +57,7 @@ class ChannelMessage {
List<Uint8List>? pathVariants,
this.channelIndex,
String? messageId,
this.packetHash,
this.replyToMessageId,
this.replyToSenderName,
this.replyToText,
@@ -79,6 +82,7 @@ class ChannelMessage {
int? pathLength,
Uint8List? pathBytes,
List<Uint8List>? pathVariants,
String? packetHash,
String? replyToMessageId,
String? replyToSenderName,
String? replyToText,
@@ -98,6 +102,7 @@ class ChannelMessage {
pathVariants: pathVariants ?? this.pathVariants,
channelIndex: channelIndex,
messageId: messageId,
packetHash: packetHash ?? this.packetHash,
replyToMessageId: replyToMessageId ?? this.replyToMessageId,
replyToSenderName: replyToSenderName ?? this.replyToSenderName,
replyToText: replyToText ?? this.replyToText,
@@ -105,89 +110,82 @@ class ChannelMessage {
);
}
static ChannelMessage? fromFrame(Uint8List data) {
static ChannelMessage? fromFrame(Uint8List frame) {
// CHANNEL_MSG_RECV format varies by version:
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
if (data.length < 8) return null;
if (frame.length < 8) return null;
try {
final reader = BufferReader(frame);
final code = reader.readByte();
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
return null;
}
final code = data[0];
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
int pathLen;
int txtType;
Uint8List pathBytes = Uint8List(0);
int channelIdx;
if (code == respCodeChannelMsgRecvV3) {
reader.skipBytes(1); // Skip SNR
final flags = reader.readByte();
final hasPath = (flags & 0x01) != 0;
reader.skipBytes(1); // Skip reserved byte
channelIdx = reader.readByte();
pathLen = reader.readInt8();
txtType = reader.readByte();
if (hasPath && pathLen > 0) {
reader.rewind(); // Rewind to read path length again for pathBytes
pathBytes = reader.readBytes(pathLen);
}
} else {
channelIdx = reader.readByte();
pathLen = reader.readInt8();
txtType = reader.readByte();
}
final timestampRaw = reader.readUInt32LE();
if (txtType != txtTypePlain) {
return null;
}
final text = reader.readCString();
// Extract sender name and actual message from "name: msg" format
String senderName = 'Unknown';
String actualText = text;
final colonIndex = text.indexOf(':');
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
final potentialSender = text.substring(0, colonIndex);
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
senderName = potentialSender;
final offset =
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
? colonIndex + 2
: colonIndex + 1;
actualText = text.substring(offset);
}
}
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: decodedText,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
status: ChannelMessageStatus.sent,
pathLength: pathLen,
pathBytes: pathBytes,
channelIndex: channelIdx,
);
} catch (e) {
appLogger.error('Error parsing channel message frame: $e');
// If parsing fails, return null to avoid crashes
return null;
}
int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
Uint8List pathBytes = Uint8List(0);
int channelIdx;
if (code == respCodeChannelMsgRecvV3) {
channelIdx = data[4];
pathLenOffset = 5;
final pathLen = data[pathLenOffset].toSigned(8);
var cursor = 6;
final hasPathBytesFlag = (data[2] & 0x01) != 0;
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
final hasValidTxtType =
cursor < data.length &&
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
canFitPath) {
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
cursor += pathLen;
}
txtTypeOffset = cursor;
cursor += 1; // txt_type
timestampOffset = cursor;
textOffset = cursor + 4;
} else {
channelIdx = data[1];
pathLenOffset = 2;
txtTypeOffset = 3;
timestampOffset = 4;
textOffset = 8;
}
if (data.length < textOffset + 1) return null;
final txtType = data[txtTypeOffset];
if (txtType != txtTypePlain) {
return null;
}
final pathLen = data[pathLenOffset].toSigned(8);
final timestampRaw = readUint32LE(data, timestampOffset);
final text = readCString(data, textOffset, data.length - textOffset);
// Extract sender name and actual message from "name: msg" format
String senderName = 'Unknown';
String actualText = text;
final colonIndex = text.indexOf(':');
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
final potentialSender = text.substring(0, colonIndex);
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
senderName = potentialSender;
final offset =
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
? colonIndex + 2
: colonIndex + 1;
actualText = text.substring(offset);
}
}
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: decodedText,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
status: ChannelMessageStatus.sent,
pathLength: pathLen,
pathBytes: pathBytes,
channelIndex: channelIdx,
);
}
static ChannelMessage outgoing(
+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;
}
}
}
+73 -67
View File
@@ -1,4 +1,6 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class Contact {
@@ -15,6 +17,9 @@ class Contact {
final double? longitude;
final DateTime lastSeen;
final DateTime lastMessageAt;
final bool isActive;
final bool wasPulled;
final Uint8List? rawPacket;
Contact({
required this.publicKey,
@@ -29,6 +34,9 @@ class Contact {
this.longitude,
required this.lastSeen,
DateTime? lastMessageAt,
this.isActive = true,
this.wasPulled = false,
this.rawPacket,
}) : lastMessageAt = lastMessageAt ?? lastSeen;
String get publicKeyHex => pubKeyToHex(publicKey);
@@ -59,7 +67,17 @@ class Contact {
return '$pathLength hops';
}
bool get hasLocation => latitude != null && longitude != null;
bool get hasLocation {
const double epsilon = 1e-6;
final lat = latitude ?? 0.0;
final lon = longitude ?? 0.0;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
bool get isFavorite => (flags & contactFlagFavorite) != 0;
Contact copyWith({
@@ -76,6 +94,8 @@ class Contact {
double? longitude,
DateTime? lastSeen,
DateTime? lastMessageAt,
bool? isActive,
Uint8List? rawPacket,
}) {
return Contact(
publicKey: publicKey ?? this.publicKey,
@@ -94,18 +114,19 @@ class Contact {
longitude: longitude ?? this.longitude,
lastSeen: lastSeen ?? this.lastSeen,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
isActive: isActive ?? this.isActive,
rawPacket: rawPacket ?? this.rawPacket,
);
}
String get pathIdList {
final pathBytes = _pathBytesForDisplay;
/// Formats path bytes into comma-separated hex groups of [hashByteWidth] bytes.
String pathFormattedIdList(int hashByteWidth) {
final pathBytes = pathBytesForDisplay;
if (pathBytes.isEmpty) return '';
final w = hashByteWidth.clamp(1, 8);
final parts = <String>[];
final groupSize = pathHashSize;
for (int i = 0; i < pathBytes.length; i += groupSize) {
final end = (i + groupSize) <= pathBytes.length
? (i + groupSize)
: pathBytes.length;
for (int i = 0; i < pathBytes.length; i += w) {
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
final chunk = pathBytes.sublist(i, end);
parts.add(
chunk
@@ -116,47 +137,14 @@ class Contact {
return parts.join(',');
}
/// Default grouping uses legacy single-byte hop hash width.
String get pathIdList => pathFormattedIdList(pathHashSize);
String get shortPubKeyHex {
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
}
Uint8List? get traceRouteBytes {
final pathBytes = _pathBytesForDisplay;
Uint8List? traceBytes;
if (pathBytes.isEmpty) {
traceBytes = Uint8List(1);
traceBytes[0] = publicKey[0];
return traceBytes;
}
if (type == advTypeRepeater || type == advTypeRoom) {
final len = (pathBytes.length + pathBytes.length + 1);
traceBytes = Uint8List(len);
traceBytes[pathBytes.length] = publicKey[0];
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
} else {
if (pathBytes.length < 2) {
return pathBytes[0] == 0 ? null : pathBytes;
}
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
}
return traceBytes;
}
Uint8List get _pathBytesForDisplay {
Uint8List get pathBytesForDisplay {
if (pathOverride != null) {
if (pathOverride! < 0) return Uint8List(0);
return pathOverrideBytes ?? Uint8List(0);
@@ -166,31 +154,44 @@ class Contact {
static Contact? fromFrame(Uint8List data) {
if (data.isEmpty) return null;
if (data[0] != respCodeContact) return null;
final reader = BufferReader(data);
try {
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final flags = data[contactFlagsOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final respCode = reader.readByte();
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
return null;
}
final pubKey = reader.readBytes(pubKeySize);
// Guard: reject contacts with zeroed or mostly-zeroed public keys
// (indicates corrupt flash storage on the firmware side)
final zeroCount = pubKey.where((b) => b == 0).length;
if (zeroCount > pubKeySize ~/ 2) return null;
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastmodOffset);
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);
// Guard: reject contacts with non-printable names (corrupt flash data)
if (name.isNotEmpty &&
name.codeUnits.every((c) => c < 0x20 || c == 0xFFFD)) {
return null;
}
final lastMod = reader.readUInt32LE();
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
if (reader.remaining >= 8) {
final latRaw = reader.readInt32LE();
final lonRaw = reader.readInt32LE();
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
}
}
return Contact(
@@ -198,14 +199,16 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen,
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
isActive: true,
rawPacket: null,
);
} catch (e) {
// If parsing fails, return null
appLogger.error('Failed to parse contact frame: $e');
return null;
}
}
@@ -216,4 +219,7 @@ class Contact {
@override
int get hashCode => publicKeyHex.hashCode;
bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
}
+43
View File
@@ -0,0 +1,43 @@
class DeliveryObservation {
final String contactKey;
final int pathLength;
final int messageBytes;
final int secondsSinceLastRx;
final bool isFlood;
final int deliveryMs;
final DateTime timestamp;
DeliveryObservation({
required this.contactKey,
required this.pathLength,
required this.messageBytes,
required this.secondsSinceLastRx,
required this.isFlood,
required this.deliveryMs,
required this.timestamp,
});
Map<String, dynamic> toJson() {
return {
'contact_key': contactKey,
'path_length': pathLength,
'message_bytes': messageBytes,
'seconds_since_last_rx': secondsSinceLastRx,
'is_flood': isFlood,
'delivery_ms': deliveryMs,
'timestamp': timestamp.toIso8601String(),
};
}
factory DeliveryObservation.fromJson(Map<String, dynamic> json) {
return DeliveryObservation(
contactKey: json['contact_key'] as String,
pathLength: json['path_length'] as int,
messageBytes: json['message_bytes'] as int,
secondsSinceLastRx: json['seconds_since_last_rx'] as int? ?? 0,
isFlood: json['is_flood'] as bool,
deliveryMs: json['delivery_ms'] as int,
timestamp: DateTime.parse(json['timestamp'] as String),
);
}
}
+34 -27
View File
@@ -16,13 +16,14 @@ class Message {
final String? messageId;
final int retryCount;
final int? estimatedTimeoutMs;
final Uint8List? expectedAckHash;
final int? expectedAckHash;
final DateTime? sentAt;
final DateTime? deliveredAt;
final int? tripTimeMs;
final int? pathLength;
final Uint8List pathBytes;
final Map<String, int> reactions;
final Map<String, MessageStatus> reactionStatuses;
final Uint8List fourByteRoomContactKey;
Message({
@@ -43,9 +44,11 @@ class Message {
Uint8List? pathBytes,
Uint8List? fourByteRoomContactKey,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
}) : pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
reactions = reactions ?? {};
reactions = reactions ?? {},
reactionStatuses = reactionStatuses ?? {};
String get senderKeyHex => pubKeyToHex(senderKey);
@@ -53,7 +56,7 @@ class Message {
MessageStatus? status,
int? retryCount,
int? estimatedTimeoutMs,
Uint8List? expectedAckHash,
int? expectedAckHash,
DateTime? sentAt,
DateTime? deliveredAt,
int? tripTimeMs,
@@ -61,6 +64,7 @@ class Message {
Uint8List? pathBytes,
bool? isCli,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
Uint8List? fourByteRoomContactKey,
}) {
return Message(
@@ -80,38 +84,41 @@ class Message {
pathLength: pathLength ?? this.pathLength,
pathBytes: pathBytes ?? this.pathBytes,
reactions: reactions ?? this.reactions,
reactionStatuses: reactionStatuses ?? this.reactionStatuses,
fourByteRoomContactKey:
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
);
}
static Message? fromFrame(Uint8List data, Uint8List selfPubKey) {
if (data.length < msgTextOffset + 1) return null;
static Message? fromFrame(Uint8List frame, Uint8List selfPubKey) {
if (frame.length < msgTextOffset + 1) return null;
final reader = BufferReader(frame);
try {
final code = reader.readByte();
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
return null;
}
final code = data[0];
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
final senderKey = reader.readBytes(pubKeySize);
final timestampRaw = reader.readInt32LE();
final flags = reader.readByte();
if ((flags >> 2) != txtTypePlain) {
return null;
}
final text = reader.readCString();
return Message(
senderKey: senderKey,
text: text,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
isCli: false,
status: MessageStatus.delivered,
pathBytes: Uint8List(0),
);
} catch (e) {
return null;
}
final senderKey = Uint8List.fromList(
data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
);
final timestampRaw = readUint32LE(data, msgTimestampOffset);
final flags = data[msgFlagsOffset];
if ((flags >> 2) != txtTypePlain) {
return null;
}
final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
return Message(
senderKey: senderKey,
text: text,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
isCli: false,
status: MessageStatus.delivered,
pathBytes: Uint8List(0),
);
}
static Message outgoing(
+9 -3
View File
@@ -1,11 +1,12 @@
class PathRecord {
final int hopCount;
final int tripTimeMs;
final DateTime timestamp;
final DateTime? timestamp;
final bool wasFloodDiscovery;
final List<int> pathBytes;
final int successCount;
final int failureCount;
final double routeWeight;
PathRecord({
required this.hopCount,
@@ -15,6 +16,7 @@ class PathRecord {
required this.pathBytes,
required this.successCount,
required this.failureCount,
this.routeWeight = 1.0,
});
String get displayText =>
@@ -24,11 +26,12 @@ class PathRecord {
return {
'hop_count': hopCount,
'trip_time_ms': tripTimeMs,
'timestamp': timestamp.toIso8601String(),
'timestamp': timestamp?.toIso8601String(),
'was_flood': wasFloodDiscovery,
'path_bytes': pathBytes,
'success_count': successCount,
'failure_count': failureCount,
'route_weight': routeWeight,
};
}
@@ -36,12 +39,15 @@ class PathRecord {
return PathRecord(
hopCount: json['hop_count'] as int,
tripTimeMs: json['trip_time_ms'] as int,
timestamp: DateTime.parse(json['timestamp'] as String),
timestamp: json['timestamp'] != null
? DateTime.parse(json['timestamp'] as String)
: null,
wasFloodDiscovery: json['was_flood'] as bool,
pathBytes:
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
);
}
}
+41
View File
@@ -1,3 +1,9 @@
import 'dart:typed_data';
import 'contact.dart';
const int recentAttemptDiversityWindow = 2;
class PathSelection {
final List<int> pathBytes;
final int hopCount;
@@ -9,3 +15,38 @@ class PathSelection {
required this.useFlood,
});
}
PathSelection resolvePathSelection(
Contact contact, {
PathSelection? selection,
bool forceFlood = false,
}) {
if (contact.pathOverride != null) {
if (contact.pathOverride! < 0) {
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
}
return PathSelection(
pathBytes: contact.pathOverrideBytes ?? Uint8List(0),
hopCount: contact.pathOverride!,
useFlood: false,
);
}
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
}
if (selection != null && selection.pathBytes.isNotEmpty) {
return PathSelection(
pathBytes: selection.pathBytes,
hopCount: selection.hopCount,
useFlood: false,
);
}
return PathSelection(
pathBytes: contact.path,
hopCount: contact.pathLength,
useFlood: false,
);
}
+138
View File
@@ -291,6 +291,14 @@ class AppSettingsScreen extends StatelessWidget {
},
),
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),
@@ -310,6 +318,118 @@ class AppSettingsScreen extends StatelessWidget {
);
},
),
if (settingsService.settings.autoRouteRotationEnabled) ...[
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_maxRouteWeight),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_maxRouteWeightSubtitle),
Slider(
value: settingsService.settings.maxRouteWeight,
min: 1,
max: 10,
divisions: 9,
label: settingsService.settings.maxRouteWeight
.round()
.toString(),
onChanged: (value) =>
settingsService.setMaxRouteWeight(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_initialRouteWeight),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_initialRouteWeightSubtitle),
Slider(
value: settingsService.settings.initialRouteWeight,
min: 0.5,
max: 5.0,
divisions: 9,
label: settingsService.settings.initialRouteWeight
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setInitialRouteWeight(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_routeWeightSuccessIncrement),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context
.l10n
.appSettings_routeWeightSuccessIncrementSubtitle,
),
Slider(
value: settingsService.settings.routeWeightSuccessIncrement,
min: 0.1,
max: 2.0,
divisions: 19,
label: settingsService.settings.routeWeightSuccessIncrement
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setRouteWeightSuccessIncrement(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_routeWeightFailureDecrement),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context
.l10n
.appSettings_routeWeightFailureDecrementSubtitle,
),
Slider(
value: settingsService.settings.routeWeightFailureDecrement,
min: 0.1,
max: 2.0,
divisions: 19,
label: settingsService.settings.routeWeightFailureDecrement
.toStringAsFixed(1),
onChanged: (value) =>
settingsService.setRouteWeightFailureDecrement(value),
),
],
),
),
const Divider(height: 1),
ListTile(
title: Text(context.l10n.appSettings_maxMessageRetries),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_maxMessageRetriesSubtitle),
Slider(
value: settingsService.settings.maxMessageRetries
.toDouble(),
min: 2,
max: 10,
divisions: 8,
label: settingsService.settings.maxMessageRetries
.toString(),
onChanged: (value) =>
settingsService.setMaxMessageRetries(value.toInt()),
),
],
),
),
],
],
),
);
@@ -577,6 +697,12 @@ class AppSettingsScreen extends StatelessWidget {
return context.l10n.appSettings_languageRu;
case 'uk':
return context.l10n.appSettings_languageUk;
case 'hu':
return context.l10n.appSettings_languageHu;
case 'ja':
return context.l10n.appSettings_languageJa;
case 'ko':
return context.l10n.appSettings_languageKo;
default:
return context.l10n.appSettings_languageSystem;
}
@@ -664,6 +790,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',
),
],
),
),
+68 -55
View File
@@ -118,6 +118,19 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
: Icons.download,
size: 18,
),
onLongPress: () async {
await Clipboard.setData(
ClipboardData(
text: entry.payload
.map(
(b) => b
.toRadixString(16)
.padLeft(2, '0'),
)
.join(''),
),
);
},
);
}
@@ -270,66 +283,66 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
if (payload.length < 101) {
return 'ADVERT (short)';
}
var offset = 0;
final pubKey = _bytesToHex(
payload.sublist(offset, offset + 32),
spaced: false,
);
offset += 32;
final timestamp = readUint32LE(payload, offset);
offset += 4;
offset += 64; // signature
final flags = payload[offset++];
final role = _deviceRoleLabel(flags & 0x0F);
final hasLocation = (flags & 0x10) != 0;
final hasFeature1 = (flags & 0x20) != 0;
final hasFeature2 = (flags & 0x40) != 0;
final hasName = (flags & 0x80) != 0;
String? name;
double? lat;
double? lon;
if (hasLocation && payload.length >= offset + 8) {
lat = readInt32LE(payload, offset) / 1000000.0;
lon = readInt32LE(payload, offset + 4) / 1000000.0;
offset += 8;
final reader = BufferReader(payload);
try {
final pubKey = _bytesToHex(reader.readBytes(pubKeySize), spaced: false);
final timestamp = reader.readUInt32LE();
reader.skipBytes(signatureSize);
final flags = reader.readByte();
final role = _deviceRoleLabel(flags & 0x0F);
final hasLocation = (flags & 0x10) != 0;
final hasFeature1 = (flags & 0x20) != 0;
final hasFeature2 = (flags & 0x40) != 0;
final hasName = (flags & 0x80) != 0;
String? name;
double? lat;
double? lon;
if (hasLocation) {
lat = reader.readInt32LE() / 1000000.0;
lon = reader.readInt32LE() / 1000000.0;
}
if (hasFeature1) reader.skipBytes(2);
if (hasFeature2) reader.skipBytes(2);
if (hasName) {
name = reader.readCStringGreedy(maxNameSize);
}
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
final locPart = (lat != null && lon != null)
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
: '';
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}';
} catch (e) {
return 'ADVERT (invalid)';
}
if (hasFeature1) offset += 2;
if (hasFeature2) offset += 2;
if (hasName && payload.length > offset) {
final rawName = String.fromCharCodes(payload.sublist(offset));
final nul = rawName.indexOf('\u0000');
name = nul >= 0 ? rawName.substring(0, nul) : rawName;
name = name.trim();
}
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
final locPart = (lat != null && lon != null)
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
: '';
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}';
}
String _decodeControlSummary(Uint8List payload) {
if (payload.isEmpty) return 'CONTROL (empty)';
final flags = payload[0];
final subType = flags & 0xF0;
if (subType == 0x80) {
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
final typeFilter = payload[1];
final tag = readUint32LE(payload, 2);
final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0;
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
final reader = BufferReader(payload);
try {
final flags = reader.readByte();
final subType = flags & 0xF0;
if (subType == 0x80) {
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
final typeFilter = reader.readByte();
final tag = reader.readInt32LE();
final since = payload.length >= 10 ? reader.readInt32LE() : 0;
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
}
if (subType == 0x90) {
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
final nodeType = flags & 0x0F;
final snrRaw = payload[1];
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
final snr = snrSigned / 4.0;
final tag = reader.readInt32LE();
final keyLen = payload.length - 6;
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
}
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
} catch (e) {
return 'CONTROL (invalid)';
}
if (subType == 0x90) {
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
final nodeType = flags & 0x0F;
final snrRaw = payload[1];
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
final snr = snrSigned / 4.0;
final tag = readUint32LE(payload, 2);
final keyLen = payload.length - 6;
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
}
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
}
String _payloadTypeLabel(int payloadType) {
+100 -20
View File
@@ -4,11 +4,11 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/link_handler.dart';
@@ -26,6 +26,7 @@ import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_status_icon.dart';
import '../widgets/radio_stats_entry.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -47,6 +48,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
bool _isLoadingOlder = false;
MeshCoreConnector? _connector;
DateTime? _lastChannelSendAt;
bool _channelSkipNextBottomSnap = false;
@override
void initState() {
@@ -55,11 +58,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();
@@ -166,6 +203,34 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
),
centerTitle: false,
actions: [
const RadioStatsIconButton(),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'clearChat') {
context.read<MeshCoreConnector>().clearMessagesForChannel(
widget.channel.index,
);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'clearChat',
child: Row(
children: [
const Icon(Icons.delete, size: 20, color: Colors.red),
const SizedBox(width: 12),
Text(
context.l10n.contact_clearChat,
style: const TextStyle(color: Colors.red),
),
],
),
),
],
),
],
),
body: SafeArea(
top: false,
@@ -216,6 +281,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();
});
@@ -311,8 +380,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
Flexible(
child: GestureDetector(
onTap: () => _showMessagePathInfo(message),
onTap: PlatformInfo.isDesktop
? null
: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
onSecondaryTapUp: PlatformInfo.isDesktop
? (_) => _showMessageActions(message)
: null,
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
@@ -430,25 +504,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Linkify(
child: LinkHandler.buildLinkifyText(
context: context,
text: message.text,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
fontSize: bodyFontSize * textScale,
color: Colors.green,
decoration: TextDecoration.underline,
),
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) => LinkHandler.handleLinkTap(
context,
link.url,
),
),
),
if (!enableTracing && isOutgoing) ...[
@@ -557,7 +618,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
);
if (!isOutgoing) {
if (!isOutgoing && !PlatformInfo.isDesktop) {
return _SwipeReplyBubble(
maxSwipeOffset: maxSwipeOffset,
replySwipeThreshold: replySwipeThreshold,
@@ -1055,6 +1116,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final text = _textController.text.trim();
if (text.isEmpty) return;
final now = DateTime.now();
if (_lastChannelSendAt != null &&
now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
return;
}
_lastChannelSendAt = now;
final connector = context.read<MeshCoreConnector>();
String messageText = text;
@@ -1112,6 +1183,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_setReplyingTo(message);
},
),
if (PlatformInfo.isDesktop)
ListTile(
leading: const Icon(Icons.route),
title: Text(context.l10n.chat_path),
onTap: () {
Navigator.pop(sheetContext);
_showMessagePathInfo(message);
},
),
// Can't react to your own messages
if (!message.isOutgoing)
ListTile(
+111 -48
View File
@@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp;
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
final hops = _buildPathHops(primaryPath, connector, l10n);
final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops(
primaryPath.length,
@@ -62,8 +61,12 @@ class ChannelMessagePathScreen extends StatelessWidget {
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: primaryPath,
flipPathRound: true,
reversePathRound: !message.isOutgoing && !channelMessage,
flipPathAround: true,
reversePathAround:
!(!channelMessage && !message.isOutgoing),
pathHashByteWidth: context
.read<MeshCoreConnector>()
.pathHashByteWidth,
),
),
),
@@ -302,10 +305,12 @@ class _ChannelMessagePathMapScreenState
extends State<ChannelMessagePathMapScreen> {
static const double _labelZoomThreshold = 8.5;
final MapController _mapController = MapController();
Uint8List? _selectedPath;
double _pathDistance = 0.0;
bool _showNodeLabels = true;
bool _didReceivePositionUpdate = false;
int? _focusedHopIndex;
@override
void initState() {
@@ -336,6 +341,22 @@ class _ChannelMessagePathMapScreenState
return totalDistance;
}
void _focusHop(_PathHop hop) {
if (!hop.hasLocation) return;
final targetZoom = _didReceivePositionUpdate
? max(_mapController.camera.zoom, 10.0)
: 12.0;
_mapController.move(hop.position!, targetZoom);
}
void _onHopTapped(_PathHop hop) {
_focusHop(hop);
if (!mounted) return;
setState(() {
_focusedHopIndex = hop.index;
});
}
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
@@ -364,11 +385,7 @@ class _ChannelMessagePathMapScreenState
: selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths);
final hops = _buildPathHops(
selectedPath,
connector.contacts,
context.l10n,
);
final hops = _buildPathHops(selectedPath, connector, context.l10n);
final points = <LatLng>[];
@@ -423,6 +440,7 @@ class _ChannelMessagePathMapScreenState
children: [
FlutterMap(
key: mapKey,
mapController: _mapController,
options: MapOptions(
initialCenter: initialCenter,
initialZoom: initialZoom,
@@ -474,6 +492,7 @@ class _ChannelMessagePathMapScreenState
) {
setState(() {
_selectedPath = observedPaths[index].pathBytes;
_focusedHopIndex = null;
});
}),
if (points.isEmpty)
@@ -729,8 +748,17 @@ class _ChannelMessagePathMapScreenState
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final hop = hops[index];
final isFocused = _focusedHopIndex == hop.index;
return ListTile(
dense: true,
enabled: hop.hasLocation,
selected: isFocused,
selectedTileColor: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.12),
onTap: hop.hasLocation
? () => _onHopTapped(hop)
: null,
leading: CircleAvatar(
radius: 14,
child: Text(
@@ -789,19 +817,83 @@ class _ObservedPath {
List<_PathHop> _buildPathHops(
Uint8List pathBytes,
List<Contact> contacts,
MeshCoreConnector connector,
AppLocalizations l10n,
) {
if (pathBytes.isEmpty) return const [];
final candidatesByPrefix = <int, List<Contact>>{};
final allContacts = connector.allContacts;
for (final contact in allContacts) {
if (contact.publicKey.isEmpty) continue;
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
continue;
}
final prefix = contact.publicKey.first;
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
}
for (final candidates in candidatesByPrefix.values) {
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
}
final startPoint =
(connector.selfLatitude != null && connector.selfLongitude != null)
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
: null;
var previousPosition = startPoint;
final distance = Distance();
var lastDistance = 0.0;
var bestDistance = 0.0;
final hops = <_PathHop>[];
for (var i = 0; i < pathBytes.length; i++) {
final prefix = pathBytes[i];
final contact = _matchContactForPrefix(contacts, prefix);
final searchPoint = i == 0 ? startPoint : previousPosition;
final candidates = candidatesByPrefix[pathBytes[i]];
Contact? contact;
if (candidates != null && candidates.isNotEmpty) {
var bestIndex = 0;
if (searchPoint != null) {
bestDistance = double.infinity;
for (var j = 0; j < candidates.length; j++) {
final candidate = candidates[j];
if (!candidate.hasLocation ||
candidate.latitude == null ||
candidate.longitude == null) {
continue;
}
final currentDistance = distance(
searchPoint,
LatLng(candidate.latitude!, candidate.longitude!),
);
if (currentDistance < bestDistance) {
bestDistance = currentDistance;
bestIndex = j;
}
}
}
contact = candidates.removeAt(bestIndex);
if (candidates.isEmpty) {
candidatesByPrefix.remove(pathBytes[i]);
}
}
final resolvedPosition = _resolvePosition(contact);
if (resolvedPosition != null) {
previousPosition = resolvedPosition;
}
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
if (lastDistance + bestDistance > 70000 &&
candidates != null &&
candidates.isNotEmpty) {
i--;
lastDistance = bestDistance;
continue;
}
lastDistance = bestDistance;
hops.add(
_PathHop(
index: i + 1,
prefix: prefix,
prefix: pathBytes[i],
contact: contact,
position: _resolvePosition(contact),
position: resolvedPosition,
l10n: l10n,
),
);
@@ -809,42 +901,13 @@ List<_PathHop> _buildPathHops(
return hops;
}
Contact? _matchContactForPrefix(List<Contact> contacts, int prefix) {
final matches = contacts
.where(
(contact) =>
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
contact.publicKey.isNotEmpty &&
contact.publicKey[0] == prefix,
)
.toList();
if (matches.isEmpty) return null;
Contact? pickWhere(bool Function(Contact) predicate) {
for (final contact in matches) {
if (predicate(contact)) return contact;
}
return null;
}
return pickWhere((c) => c.type == advTypeRepeater && _hasValidLocation(c)) ??
pickWhere((c) => c.type == advTypeRepeater) ??
pickWhere(_hasValidLocation) ??
matches.first;
}
LatLng? _resolvePosition(Contact? contact) {
if (contact == null) return null;
if (!_hasValidLocation(contact)) return null;
return LatLng(contact.latitude!, contact.longitude!);
}
bool _hasValidLocation(Contact contact) {
final lat = contact.latitude;
final lon = contact.longitude;
if (lat == null || lon == null) return false;
if (lat == 0 && lon == 0) return false;
return true;
if (!contact.hasLocation) return null;
final latitude = contact.latitude;
final longitude = contact.longitude;
if (latitude == null || longitude == null) return null;
return LatLng(latitude, longitude);
}
String _formatPrefix(int prefix) {
+164 -132
View File
@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/storage/channel_message_store.dart';
import 'package:meshcore_open/utils/platform_info.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
@@ -11,6 +12,7 @@ import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../services/ui_view_state_service.dart';
import '../models/channel.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
@@ -28,8 +30,6 @@ import 'contacts_screen.dart';
import 'map_screen.dart';
import 'settings_screen.dart';
enum ChannelSortOption { manual, name, latestMessages, unread }
class ChannelsScreen extends StatefulWidget {
final bool hideBackButton;
@@ -43,17 +43,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
with DisconnectNavigationMixin {
final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore();
String _searchQuery = '';
Timer? _searchDebounce;
ChannelSortOption _sortOption = ChannelSortOption.manual;
List<Community> _communities = [];
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
@override
void initState() {
super.initState();
_searchController.text = context
.read<UiViewStateService>()
.channelsSearchText;
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MeshCoreConnector>().getChannels();
_loadCommunities();
@@ -61,6 +64,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
Future<void> _loadCommunities() async {
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
final communities = await _communityStore.loadCommunities();
if (mounted) {
setState(() {
@@ -106,7 +111,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final viewState = context.watch<UiViewStateService>();
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
@@ -199,6 +207,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final filteredChannels = _filterAndSortChannels(
channels,
connector,
viewState,
);
return Column(
@@ -213,17 +222,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_searchQuery.isNotEmpty)
if (viewState.channelsSearchText.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchDebounce?.cancel();
_searchDebounce = null;
_searchController.clear();
setState(() {
_searchQuery = '';
});
context
.read<UiViewStateService>()
.setChannelsSearchText('');
},
),
_buildFilterButton(),
_buildFilterButton(viewState),
],
),
border: OutlineInputBorder(
@@ -240,9 +251,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
const Duration(milliseconds: 300),
() {
if (!mounted) return;
setState(() {
_searchQuery = value.toLowerCase();
});
context
.read<UiViewStateService>()
.setChannelsSearchText(value);
},
);
},
@@ -277,8 +288,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
],
)
: (_sortOption == ChannelSortOption.manual &&
_searchQuery.isEmpty)
: (viewState.channelsSortOption ==
ChannelSortOption.manual &&
viewState.channelsSearchText.isEmpty)
? ReorderableListView.builder(
padding: const EdgeInsets.only(
left: 16,
@@ -406,78 +418,96 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return Card(
key: ValueKey('channel_${channel.index}'),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
dense: true,
minVerticalPadding: 0,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
visualDensity: const VisualDensity(vertical: -2),
leading: Stack(
children: [
CircleAvatar(
backgroundColor: bgColor,
child: Icon(icon, color: iconColor),
),
if (isCommunityChannel)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).cardColor,
width: 2,
child: GestureDetector(
onSecondaryTapUp: PlatformInfo.isDesktop
? (_) => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
)
: null,
child: ListTile(
dense: true,
minVerticalPadding: 0,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
visualDensity: const VisualDensity(vertical: -2),
leading: Stack(
children: [
CircleAvatar(
backgroundColor: bgColor,
child: Icon(icon, color: iconColor),
),
if (isCommunityChannel)
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).cardColor,
width: 2,
),
),
child: const Icon(
Icons.people,
size: 8,
color: Colors.white,
),
),
child: const Icon(Icons.people, size: 8, color: Colors.white),
),
),
],
),
title: Text(
channel.name.isEmpty
? context.l10n.channels_channelIndex(channel.index)
: channel.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(width: 4),
],
if (showDragHandle && dragIndex != null)
ReorderableDelayedDragStartListener(
index: dragIndex,
child: Icon(
Icons.drag_handle,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
title: Text(
channel.name.isEmpty
? context.l10n.channels_channelIndex(channel.index)
: channel.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(width: 4),
],
if (showDragHandle && dragIndex != null)
ReorderableDelayedDragStartListener(
index: dragIndex,
child: Icon(
Icons.drag_handle,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
onTap: () async {
connector.markChannelRead(channel.index);
await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelChatScreen(channel: channel),
),
);
}
},
onLongPress: () => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
],
),
onTap: () async {
connector.markChannelRead(channel.index);
await Future.delayed(const Duration(milliseconds: 50));
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelChatScreen(channel: channel),
),
);
}
},
onLongPress: () => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
),
),
),
);
@@ -578,59 +608,40 @@ class _ChannelsScreenState extends State<ChannelsScreen>
await showDisconnectDialog(context, connector);
}
Widget _buildFilterButton() {
const actionSortManual = 0;
const actionSortName = 1;
const actionSortLatest = 2;
const actionSortUnread = 3;
return SortFilterMenu(
Widget _buildFilterButton(UiViewStateService viewState) {
return SortFilterMenu<ChannelSortOption>(
tooltip: context.l10n.listFilter_tooltip,
sections: [
SortFilterMenuSection(
SortFilterMenuSection<ChannelSortOption>(
title: context.l10n.channels_sortBy,
options: [
SortFilterMenuOption(
value: actionSortManual,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.manual,
label: context.l10n.channels_sortManual,
checked: _sortOption == ChannelSortOption.manual,
checked: viewState.channelsSortOption == ChannelSortOption.manual,
),
SortFilterMenuOption(
value: actionSortName,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.name,
label: context.l10n.channels_sortAZ,
checked: _sortOption == ChannelSortOption.name,
checked: viewState.channelsSortOption == ChannelSortOption.name,
),
SortFilterMenuOption(
value: actionSortLatest,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.latestMessages,
label: context.l10n.channels_sortLatestMessages,
checked: _sortOption == ChannelSortOption.latestMessages,
checked:
viewState.channelsSortOption ==
ChannelSortOption.latestMessages,
),
SortFilterMenuOption(
value: actionSortUnread,
SortFilterMenuOption<ChannelSortOption>(
value: ChannelSortOption.unread,
label: context.l10n.channels_sortUnread,
checked: _sortOption == ChannelSortOption.unread,
checked: viewState.channelsSortOption == ChannelSortOption.unread,
),
],
),
],
onSelected: (action) {
setState(() {
switch (action) {
case actionSortManual:
_sortOption = ChannelSortOption.manual;
break;
case actionSortLatest:
_sortOption = ChannelSortOption.latestMessages;
break;
case actionSortUnread:
_sortOption = ChannelSortOption.unread;
break;
case actionSortName:
default:
_sortOption = ChannelSortOption.name;
break;
}
});
onSelected: (sortOption) {
viewState.setChannelsSortOption(sortOption);
},
);
}
@@ -638,11 +649,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
List<Channel> _filterAndSortChannels(
List<Channel> channels,
MeshCoreConnector connector,
UiViewStateService viewState,
) {
var filtered = channels.where((channel) {
if (_searchQuery.isEmpty) return true;
if (viewState.channelsSearchText.isEmpty) return true;
final label = _normalizeChannelName(channel);
return label.toLowerCase().contains(_searchQuery);
return label.toLowerCase().contains(
viewState.channelsSearchText.toLowerCase(),
);
}).toList();
int compareByName(Channel a, Channel b) {
@@ -651,7 +665,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
}
switch (_sortOption) {
switch (viewState.channelsSortOption) {
case ChannelSortOption.manual:
break;
case ChannelSortOption.latestMessages:
@@ -712,6 +726,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
bool isRegularHashtag = true;
Community? selectedCommunity;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
@@ -763,7 +779,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
);
}
Widget? buildExpandedContent() {
Widget? buildExpandedContent(
ChannelMessageStore channelMessageStore,
) {
switch (selectedOption) {
case 0: // Create Private Channel
return Column(
@@ -788,7 +806,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
children: [
Expanded(
child: FilledButton(
onPressed: () {
onPressed: () async {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(
@@ -810,7 +828,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
psk[i] = random.nextInt(256);
}
Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk);
await connector.setChannel(
nextIndex,
name,
psk,
);
await channelMessageStore.clearChannelMessages(
nextIndex,
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -1329,7 +1354,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_createPrivateChannelDesc,
),
if (selectedOption == 0) buildExpandedContent()!,
if (selectedOption == 0)
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 1,
@@ -1338,7 +1364,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinPrivateChannelDesc,
),
if (selectedOption == 1) buildExpandedContent()!,
if (selectedOption == 1)
buildExpandedContent(_channelMessageStore)!,
if (!hasPublicChannel) ...[
const Divider(height: 1),
buildOptionTile(
@@ -1348,7 +1375,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinPublicChannelDesc,
),
if (selectedOption == 2) buildExpandedContent()!,
if (selectedOption == 2)
buildExpandedContent(_channelMessageStore)!,
],
const Divider(height: 1),
buildOptionTile(
@@ -1358,7 +1386,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinHashtagChannelDesc,
),
if (selectedOption == 3) buildExpandedContent()!,
if (selectedOption == 3)
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 4,
@@ -1366,7 +1395,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_scanQr,
subtitle: dialogContext.l10n.community_join,
),
if (selectedOption == 4) buildExpandedContent()!,
if (selectedOption == 4)
buildExpandedContent(_channelMessageStore)!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 5,
@@ -1374,7 +1404,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_create,
subtitle: dialogContext.l10n.community_createDesc,
),
if (selectedOption == 5) buildExpandedContent()!,
if (selectedOption == 5)
buildExpandedContent(_channelMessageStore)!,
],
),
),
@@ -1524,7 +1555,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try {
await connector.deleteChannel(channel.index);
channelMessageStore.clearChannelMessages(channel.index);
await channelMessageStore.clearChannelMessages(channel.index);
if (!context.mounted) return;
@@ -1749,6 +1780,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
final channelCount = communityChannels.length;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog(
context: context,
+470 -139
View File
@@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:provider/provider.dart';
import '../utils/platform_info.dart';
import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart';
@@ -16,6 +17,7 @@ import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart';
import '../helpers/link_handler.dart';
import '../helpers/path_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
@@ -34,8 +36,10 @@ import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/path_selection_dialog.dart';
import '../widgets/radio_stats_entry.dart';
import '../utils/app_logger.dart';
import '../l10n/l10n.dart';
import 'telemetry_screen.dart';
class ChatScreen extends StatefulWidget {
final Contact contact;
@@ -50,8 +54,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() {
@@ -60,11 +67,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();
@@ -106,10 +152,9 @@ class _ChatScreenState extends State<ChatScreen> {
final unreadLabel = context.l10n.chat_unread(unreadCount);
final pathLabel = _currentPathLabel(contact);
// Show path details if we have path data (from device or override)
final hasPathData =
contact.path.isNotEmpty || contact.pathOverrideBytes != null;
// Show path details if we have non-empty path data (from device or override)
final effectivePath = contact.pathOverrideBytes ?? contact.path;
final hasPathData = effectivePath.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -143,12 +188,25 @@ class _ChatScreenState extends State<ChatScreen> {
final contact = _resolveContact(connector);
final isFloodMode = contact.pathOverride == -1;
final isDirectMode = contact.pathOverride == 0;
final activeMode = isFloodMode
? 'flood'
: isDirectMode
? 'direct'
: 'auto';
return PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: context.l10n.chat_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(contact, pathLen: -1);
} else if (mode == 'direct') {
await connector.setPathOverride(
contact,
pathLen: 0,
pathBytes: Uint8List(0),
);
} else {
await connector.setPathOverride(contact, pathLen: null);
}
@@ -161,7 +219,7 @@ class _ChatScreenState extends State<ChatScreen> {
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
color: activeMode == 'auto'
? Theme.of(context).primaryColor
: null,
),
@@ -169,7 +227,30 @@ class _ChatScreenState extends State<ChatScreen> {
Text(
context.l10n.chat_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
fontWeight: activeMode == 'auto'
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'direct',
child: Row(
children: [
Icon(
Icons.near_me,
size: 20,
color: activeMode == 'direct'
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
context.l10n.chat_direct,
style: TextStyle(
fontWeight: activeMode == 'direct'
? FontWeight.bold
: FontWeight.normal,
),
@@ -184,7 +265,7 @@ class _ChatScreenState extends State<ChatScreen> {
Icon(
Icons.waves,
size: 20,
color: isFloodMode
color: activeMode == 'flood'
? Theme.of(context).primaryColor
: null,
),
@@ -192,7 +273,7 @@ class _ChatScreenState extends State<ChatScreen> {
Text(
context.l10n.chat_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
fontWeight: activeMode == 'flood'
? FontWeight.bold
: FontWeight.normal,
),
@@ -209,9 +290,78 @@ class _ChatScreenState extends State<ChatScreen> {
tooltip: context.l10n.chat_pathManagement,
onPressed: () => _showPathHistory(context),
),
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () => _showContactInfo(context),
const RadioStatsIconButton(),
Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
return PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'info') {
_showContactInfo(context);
}
if (value == 'settings') {
_showContactSettings(context);
}
if (value == 'telemetry') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TelemetryScreen(contact: widget.contact),
),
);
}
if (value == 'clearChat') {
connector.clearMessagesForContact(widget.contact);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'info',
child: Row(
children: [
const Icon(Icons.info_outline, size: 20),
const SizedBox(width: 12),
Text(context.l10n.contact_info),
],
),
),
PopupMenuItem(
value: 'telemetry',
child: Row(
children: [
const Icon(Icons.bar_chart, size: 20),
const SizedBox(width: 12),
Text(context.l10n.contact_telemetry),
],
),
),
PopupMenuItem(
value: 'settings',
child: Row(
children: [
const Icon(Icons.settings, size: 20),
const SizedBox(width: 12),
Text(context.l10n.contact_settings),
],
),
),
PopupMenuItem(
value: 'clearChat',
child: Row(
children: [
const Icon(Icons.delete, size: 20, color: Colors.red),
const SizedBox(width: 12),
Text(
context.l10n.contact_clearChat,
style: const TextStyle(color: Colors.red),
),
],
),
),
],
);
},
),
],
),
@@ -251,7 +401,9 @@ class _ChatScreenState extends State<ChatScreen> {
),
const SizedBox(height: 8),
Text(
context.l10n.chat_sendMessageTo(widget.contact.name),
context.l10n.chat_sendMessageTo(
_resolveContact(context.read<MeshCoreConnector>()).name,
),
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
@@ -269,6 +421,8 @@ 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();
});
@@ -293,10 +447,10 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
final messageIndex = index;
Contact contact = widget.contact;
Contact contact = _resolveContact(connector);
final message = reversedMessages[messageIndex];
String fourByteHex = '';
if (widget.contact.type == advTypeRoom) {
if (contact.type == advTypeRoom) {
contact = _resolveContactFrom4Bytes(
connector,
message.fourByteRoomContactKey.isEmpty
@@ -314,16 +468,23 @@ class _ChatScreenState extends State<ChatScreen> {
final textScale = context.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _MessageBubble(
final resolvedContact = _resolveContact(connector);
final bubble = _MessageBubble(
message: message,
senderName: widget.contact.type == advTypeRoom
senderName: resolvedContact.type == advTypeRoom
? "${contact.name} [$fourByteHex]"
: contact.name,
isRoomServer: widget.contact.type == advTypeRoom,
isRoomServer: resolvedContact.type == advTypeRoom,
textScale: textScale,
onTap: () => _openMessagePath(message, contact),
onLongPress: () => _showMessageActions(message, contact),
onRetryReaction: (msg, emoji) =>
_sendReaction(msg, contact, emoji),
);
if (identical(message, _pendingUnreadScrollTarget)) {
return KeyedSubtree(key: _unreadScrollKey, child: bubble);
}
return bubble;
},
);
},
@@ -449,6 +610,16 @@ class _ChatScreenState extends State<ChatScreen> {
final text = _textController.text.trim();
if (text.isEmpty) return;
final now = DateTime.now();
if (_lastTextSendAt != null &&
now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
return;
}
_lastTextSendAt = now;
final maxBytes = maxContactMessageBytes();
if (utf8.encode(text).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -457,7 +628,7 @@ class _ChatScreenState extends State<ChatScreen> {
return;
}
connector.sendMessage(widget.contact, text);
connector.sendMessage(_resolveContact(connector), text);
_textController.clear();
_textFieldFocusNode.requestFocus();
}
@@ -654,7 +825,7 @@ class _ChatScreenState extends State<ChatScreen> {
// Set the path override to persist user's choice
await connector.setPathOverride(
widget.contact,
_resolveContact(connector),
pathLen: pathLength,
pathBytes: pathBytes,
);
@@ -663,7 +834,7 @@ class _ChatScreenState extends State<ChatScreen> {
Navigator.pop(context);
await _notifyPathSet(
connector,
widget.contact,
_resolveContact(connector),
pathBytes,
path.hopCount,
);
@@ -722,7 +893,9 @@ class _ChatScreenState extends State<ChatScreen> {
style: const TextStyle(fontSize: 11),
),
onTap: () async {
await connector.clearContactPath(widget.contact);
await connector.clearContactPath(
_resolveContact(connector),
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -750,7 +923,7 @@ class _ChatScreenState extends State<ChatScreen> {
),
onTap: () async {
await connector.setPathOverride(
widget.contact,
_resolveContact(connector),
pathLen: -1,
);
if (!context.mounted) return;
@@ -779,7 +952,8 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
String _formatRelativeTime(DateTime time) {
String _formatRelativeTime(DateTime? time) {
if (time == null) return '';
final diff = DateTime.now().difference(time);
if (diff.inSeconds < 60) return context.l10n.time_justNow;
if (diff.inMinutes < 60) {
@@ -800,15 +974,31 @@ class _ChatScreenState extends State<ChatScreen> {
return;
}
final formattedPath = pathBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(',');
final connector = context.read<MeshCoreConnector>();
final allContacts = connector.allContacts;
final formattedPath = PathHelper.formatPathHex(pathBytes);
final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.chat_fullPath),
content: SelectableText(formattedPath),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(formattedPath),
const SizedBox(height: 8),
SelectableText(
resolvedNames,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.push(
@@ -817,7 +1007,9 @@ class _ChatScreenState extends State<ChatScreen> {
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes),
flipPathRound: true,
flipPathAround: true,
targetContact: widget.contact,
pathHashByteWidth: connector.pathHashByteWidth,
),
),
),
@@ -832,11 +1024,22 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
int _resolveContactIndex = -1;
Contact _resolveContact(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
orElse: () => widget.contact,
);
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
}
Contact _resolveContactFrom4Bytes(
@@ -889,59 +1092,127 @@ class _ChatScreenState extends State<ChatScreen> {
void _showContactInfo(BuildContext context) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
final contact = _resolveContact(connector);
showDialog(
context: context,
builder: (context) => Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final contact = _resolveContact(connector);
final smazEnabled = connector.isContactSmazEnabled(
contact.publicKeyHex,
);
return AlertDialog(
title: Text(contact.name),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
if (contact.hasLocation)
_buildInfoRow(
context.l10n.chat_location,
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
),
_buildInfoRow(
context.l10n.chat_publicKey,
'${contact.publicKeyHex.substring(0, 16)}...',
),
const Divider(),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.channels_smazCompression),
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
value: smazEnabled,
onChanged: (value) {
connector.setContactSmazEnabled(
contact.publicKeyHex,
value,
);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
builder: (context) => AlertDialog(
title: SelectableText(contact.name),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
_buildInfoRow(
context.l10n.contact_lastSeen,
_formatContactLastMessage(contact.lastMessageAt),
),
if (contact.hasLocation)
_buildInfoRow(
context.l10n.chat_location,
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
),
_buildInfoRow(context.l10n.chat_publicKey, contact.publicKeyHex),
],
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
),
);
}
void _showContactSettings(BuildContext context) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
final contact = widget.contact;
bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
bool teleBaseEnabled = contact.teleBaseEnabled;
bool teleLocEnabled = contact.teleLocEnabled;
bool teleEnvEnabled = contact.teleEnvEnabled;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(context.l10n.contact_settings),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (contact.hasLocation) ...[
_buildInfoRow(
context.l10n.chat_location,
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
),
const Divider(height: 8),
],
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.channels_smazCompression),
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
value: smazEnabled,
onChanged: (value) {
connector.setContactSmazEnabled(
contact.publicKeyHex,
value,
);
setDialogState(() => smazEnabled = value);
},
),
const Divider(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.contact_teleBase),
subtitle: Text(context.l10n.contact_teleBaseSubtitle),
value: teleBaseEnabled,
onChanged: (value) {
setDialogState(() => teleBaseEnabled = value);
},
),
const Divider(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.contact_teleLoc),
subtitle: Text(context.l10n.contact_teleLocSubtitle),
value: teleLocEnabled,
onChanged: (value) {
setDialogState(() => teleLocEnabled = value);
},
),
const Divider(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.contact_teleEnv),
subtitle: Text(context.l10n.contact_teleEnvSubtitle),
value: teleEnvEnabled,
onChanged: (value) {
setDialogState(() => teleEnvEnabled = value);
},
),
],
),
),
actions: [
TextButton(
onPressed: () {
connector.setContactFlags(
contact,
teleBase: teleBaseEnabled,
teleLoc: teleLocEnabled,
teleEnv: teleEnvEnabled,
);
Navigator.pop(context);
},
child: Text(context.l10n.common_close),
),
],
),
),
);
}
@@ -956,12 +1227,32 @@ class _ChatScreenState extends State<ChatScreen> {
width: 80,
child: Text(label, style: TextStyle(color: Colors.grey[600])),
),
Expanded(child: Text(value)),
Expanded(child: SelectableText(value)),
],
),
);
}
String _formatContactLastMessage(DateTime timestamp) {
final diff = DateTime.now().difference(timestamp);
if (diff.isNegative || diff.inMinutes < 5) {
return context.l10n.contacts_lastSeenNow;
}
if (diff.inMinutes < 60) {
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return hours == 1
? context.l10n.contacts_lastSeenHourAgo
: context.l10n.contacts_lastSeenHoursAgo(hours);
}
final days = diff.inDays;
return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
}
void _openChat(BuildContext context, Contact contact) {
// Check if this is a repeater
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
@@ -981,11 +1272,13 @@ 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
final availableContacts = connector.contacts
final availableContacts = connector.allContacts
.where((c) => c != widget.contact)
.toList();
@@ -1004,11 +1297,7 @@ class _ChatScreenState extends State<ChatScreen> {
);
if (result == null) {
appLogger.info(
'PathSelectionDialog was cancelled or returned null',
tag: 'ChatScreen',
);
return;
return; // Cancelled keep existing path
}
if (!mounted) {
@@ -1024,14 +1313,19 @@ class _ChatScreenState extends State<ChatScreen> {
tag: 'ChatScreen',
);
await connector.setPathOverride(
widget.contact,
_resolveContact(connector),
pathLen: result.length,
pathBytes: result,
);
appLogger.info('setPathOverride completed', tag: 'ChatScreen');
if (!mounted) return;
await _notifyPathSet(connector, widget.contact, result, result.length);
await _notifyPathSet(
connector,
_resolveContact(connector),
result,
result.length,
);
}
void _openMessagePath(Message message, Contact contact) {
@@ -1043,10 +1337,10 @@ class _ChatScreenState extends State<ChatScreen> {
final String senderName;
if (message.isOutgoing) {
senderName = connector.selfName ?? context.l10n.chat_me;
} else if (widget.contact.type == advTypeRoom) {
} else if (_resolveContact(connector).type == advTypeRoom) {
senderName = "${contact.name} [$fourByteHex]";
} else {
senderName = widget.contact.name;
senderName = _resolveContact(connector).name;
}
final pathMessage = ChannelMessage(
senderKey: null,
@@ -1084,6 +1378,15 @@ class _ChatScreenState extends State<ChatScreen> {
_showEmojiPicker(message, contact);
},
),
if (PlatformInfo.isDesktop)
ListTile(
leading: const Icon(Icons.route),
title: Text(context.l10n.chat_path),
onTap: () {
Navigator.pop(sheetContext);
_openMessagePath(message, contact);
},
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.common_copy),
@@ -1109,7 +1412,8 @@ class _ChatScreenState extends State<ChatScreen> {
_retryMessage(message);
},
),
if (widget.contact.type == advTypeRoom)
if (_resolveContact(context.read<MeshCoreConnector>()).type ==
advTypeRoom)
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
@@ -1147,7 +1451,7 @@ class _ChatScreenState extends State<ChatScreen> {
void _retryMessage(Message message) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Retry using the contact's current path override setting
connector.sendMessage(widget.contact, message.text);
connector.sendMessage(_resolveContact(connector), message.text);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
@@ -1173,7 +1477,8 @@ class _ChatScreenState extends State<ChatScreen> {
// For room servers, include sender name (like channels) since multiple users
// For 1:1 chats, sender is implicit (null)
final senderName = widget.contact.type == advTypeRoom
final liveContact = _resolveContact(connector);
final senderName = liveContact.type == advTypeRoom
? senderContact.name
: null;
final hash = ReactionHelper.computeReactionHash(
@@ -1182,7 +1487,7 @@ class _ChatScreenState extends State<ChatScreen> {
message.text,
);
final reactionText = 'r:$hash:$emojiIndex';
connector.sendMessage(widget.contact, reactionText);
connector.sendMessage(_resolveContact(connector), reactionText);
}
}
@@ -1192,6 +1497,7 @@ class _MessageBubble extends StatelessWidget {
final bool isRoomServer;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final void Function(Message message, String emoji)? onRetryReaction;
final double textScale;
const _MessageBubble({
@@ -1201,6 +1507,7 @@ class _MessageBubble extends StatelessWidget {
required this.textScale,
this.onTap,
this.onLongPress,
this.onRetryReaction,
});
@override
@@ -1234,8 +1541,11 @@ class _MessageBubble extends StatelessWidget {
: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: onTap,
onTap: PlatformInfo.isDesktop ? null : onTap,
onLongPress: onLongPress,
onSecondaryTapUp: PlatformInfo.isDesktop
? (_) => onLongPress?.call()
: null,
child: Row(
mainAxisAlignment: isOutgoing
? MainAxisAlignment.end
@@ -1352,26 +1662,13 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Linkify(
child: LinkHandler.buildLinkifyText(
context: context,
text: messageText,
style: TextStyle(
color: textColor,
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
color: Colors.green,
decoration: TextDecoration.underline,
fontSize: bodyFontSize * textScale,
),
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) => LinkHandler.handleLinkTap(
context,
link.url,
),
),
),
if (!enableTracing && isOutgoing) ...[
@@ -1400,7 +1697,10 @@ class _MessageBubble extends StatelessWidget {
child: Text(
context.l10n.chat_retryCount(
message.retryCount,
4,
context
.read<AppSettingsService>()
.settings
.maxMessageRetries,
),
style: TextStyle(
fontSize: 10,
@@ -1561,33 +1861,64 @@ class _MessageBubble extends StatelessWidget {
children: message.reactions.entries.map((entry) {
final emoji = entry.key;
final count = entry.value;
final status = message.reactionStatuses[emoji];
final isPending =
status == MessageStatus.pending || status == MessageStatus.sent;
final isFailed = status == MessageStatus.failed;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(emoji, style: const TextStyle(fontSize: 16)),
if (count > 1) ...[
const SizedBox(width: 4),
Text(
'$count',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.onSecondaryContainer,
),
return GestureDetector(
onTap: isFailed && onRetryReaction != null
? () => onRetryReaction!(message, emoji)
: null,
child: Opacity(
opacity: isPending ? 0.5 : 1.0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isFailed
? colorScheme.errorContainer
: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isFailed
? colorScheme.error
: colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
],
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(emoji, style: const TextStyle(fontSize: 16)),
if (count > 1) ...[
const SizedBox(width: 4),
Text(
'$count',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.onSecondaryContainer,
),
),
],
if (isPending) ...[
const SizedBox(width: 2),
SizedBox(
width: 8,
height: 8,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: colorScheme.onSecondaryContainer,
),
),
],
if (isFailed) ...[
const SizedBox(width: 2),
Icon(Icons.replay, size: 10, color: colorScheme.error),
],
],
),
),
),
);
}).toList(),
+89
View File
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
class ChromeRequiredScreen extends StatelessWidget {
const ChromeRequiredScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.browser_not_supported_rounded,
size: 80,
color: Colors.orange,
),
),
const SizedBox(height: 32),
Text(
l10n.scanner_chromeRequired,
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 16),
Text(
l10n.scanner_chromeRequiredMessage,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.white70 : Colors.black54,
height: 1.5,
),
),
const SizedBox(height: 48),
// We can't really "fix" it for them other than telling them to use Chrome
// but we can provide a nice visual.
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
const SizedBox(width: 12),
Text(
"Web Bluetooth requires a Chromium browser",
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
);
}
}
@@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
_isProcessing = true;
});
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
@@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
bool addPublicChannel,
) async {
// Save community to local storage
final connector = context.read<MeshCoreConnector>();
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/models/companion_radio_stats.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:provider/provider.dart';
class CompanionRadioStatsScreen extends StatefulWidget {
const CompanionRadioStatsScreen({super.key});
@override
State<CompanionRadioStatsScreen> createState() =>
_CompanionRadioStatsScreenState();
}
class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
final List<double> _noiseHistory = [];
static const int _maxSamples = 120;
MeshCoreConnector? _connector;
DateTime? _lastChartSampleAt;
@override
void initState() {
super.initState();
final c = context.read<MeshCoreConnector>();
_connector = c;
c.acquireRadioStatsPolling();
c.setPollingInterval(1);
c.radioStatsNotifier.addListener(_onStatsUpdate);
}
void _onStatsUpdate() {
final s = _connector?.radioStatsNotifier.value;
if (s == null || !mounted) return;
if (_lastChartSampleAt == s.receivedAt) return;
_lastChartSampleAt = s.receivedAt;
setState(() {
_noiseHistory.add(s.noiseFloorDbm.toDouble());
while (_noiseHistory.length > _maxSamples) {
_noiseHistory.removeAt(0);
}
});
}
@override
void dispose() {
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
_connector?.releaseRadioStatsPolling();
_connector?.setPollingInterval(30);
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.radioStats_screenTitle),
centerTitle: true,
),
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>(
selector: (_, c) => (
connected: c.isConnected,
supported: c.supportsCompanionRadioStats,
),
builder: (context, state, _) {
if (!state.connected) {
return Center(child: Text(l10n.radioStats_notConnected));
}
if (!state.supported) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
l10n.radioStats_firmwareTooOld,
textAlign: TextAlign.center,
),
),
);
}
final connector = context.read<MeshCoreConnector>();
final scheme = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return ValueListenableBuilder<CompanionRadioStats?>(
valueListenable: connector.radioStatsNotifier,
builder: (context, stats, _) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (stats != null) ...[
Text(
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
style: tt.titleMedium,
),
const SizedBox(height: 4),
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
Text(
l10n.radioStats_lastSnr(
stats.lastSnrDb.toStringAsFixed(1),
),
),
Text(l10n.radioStats_txAir(stats.txAirSecs)),
Text(l10n.radioStats_rxAir(stats.rxAirSecs)),
const SizedBox(height: 16),
] else
Text(l10n.radioStats_waiting),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: CustomPaint(
painter: _NoiseChartPainter(
samples: List<double>.from(_noiseHistory),
colorScheme: scheme,
textTheme: tt,
),
child: const SizedBox.expand(),
),
),
const SizedBox(height: 8),
Text(
l10n.radioStats_chartCaption,
style: tt.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
);
},
);
},
),
);
}
}
class _NoiseChartPainter extends CustomPainter {
final List<double> samples;
final ColorScheme colorScheme;
final TextTheme textTheme;
_NoiseChartPainter({
required this.samples,
required this.colorScheme,
required this.textTheme,
});
@override
void paint(Canvas canvas, Size size) {
final bg = Paint()..color = colorScheme.surfaceContainerHighest;
final border = Paint()
..color = colorScheme.outlineVariant
..style = PaintingStyle.stroke
..strokeWidth = 1;
final grid = Paint()
..color = colorScheme.outlineVariant.withValues(alpha: 0.5)
..strokeWidth = 1;
final line = Paint()
..color = colorScheme.primary
..strokeWidth = 2
..style = PaintingStyle.stroke;
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
bg,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
border,
);
const padL = 40.0;
const padR = 8.0;
const padT = 8.0;
const padB = 24.0;
final chart = Rect.fromLTRB(
padL,
padT,
size.width - padR,
size.height - padB,
);
for (var i = 0; i <= 4; i++) {
final y = chart.top + (chart.height * i / 4);
canvas.drawLine(Offset(chart.left, y), Offset(chart.right, y), grid);
}
if (samples.length < 2) {
final tp = TextPainter(
text: TextSpan(
text: '',
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(
canvas,
Offset(chart.left + 4, chart.top + chart.height / 2 - tp.height / 2),
);
return;
}
double minV = samples.reduce((a, b) => a < b ? a : b);
double maxV = samples.reduce((a, b) => a > b ? a : b);
if ((maxV - minV).abs() < 1) {
minV -= 2;
maxV += 2;
}
final span = maxV - minV;
for (var i = 0; i <= 2; i++) {
final v = maxV - span * i / 2;
final tp = _yAxisLabel(v);
final y = chart.top + (chart.height * i / 2) - tp.height / 2;
tp.paint(canvas, Offset(4, y));
}
final path = Path();
for (var i = 0; i < samples.length; i++) {
final x = chart.left + (chart.width * i / (samples.length - 1));
final t = (samples[i] - minV) / span;
final y = chart.bottom - t * chart.height;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, line);
}
@override
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
return oldDelegate.samples.length != samples.length ||
oldDelegate.colorScheme != colorScheme;
}
TextPainter _yAxisLabel(double v) {
final tp = TextPainter(
text: TextSpan(
text: v.round().toString(),
style: textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
textDirection: TextDirection.ltr,
)..layout();
return tp;
}
}
File diff suppressed because it is too large Load Diff
-280
View File
@@ -1,280 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/quick_switch_bar.dart';
import 'channels_screen.dart';
import 'contacts_screen.dart';
import 'map_screen.dart';
import 'settings_screen.dart';
/// Main hub screen after connecting to a MeshCore device
class DeviceScreen extends StatefulWidget {
const DeviceScreen({super.key});
@override
State<DeviceScreen> createState() => _DeviceScreenState();
}
class _DeviceScreenState extends State<DeviceScreen>
with DisconnectNavigationMixin {
bool _showBatteryVoltage = false;
int _quickIndex = 0;
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
}
final theme = Theme.of(context);
return PopScope(
canPop: false,
child: Scaffold(
appBar: AppBar(
leading: _buildBatteryIndicator(connector, context),
titleSpacing: 16,
centerTitle: false,
title: _buildAppBarTitle(connector, theme),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(Icons.bluetooth_disabled),
tooltip: context.l10n.common_disconnect,
onPressed: () => _disconnect(context, connector),
),
IconButton(
icon: const Icon(Icons.tune),
tooltip: context.l10n.common_settings,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
),
),
],
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
children: [
_buildConnectionCard(connector, context),
const SizedBox(height: 16),
_buildSectionLabel(theme, context.l10n.device_quickSwitch),
const SizedBox(height: 12),
_buildQuickSwitchBar(context),
],
),
),
),
);
},
);
}
Widget _buildAppBarTitle(MeshCoreConnector connector, ThemeData theme) {
final colorScheme = theme.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.device_meshcore,
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.8,
color: colorScheme.onSurfaceVariant,
),
),
Text(
connector.deviceDisplayName,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
);
}
Widget _buildSectionLabel(ThemeData theme, String text) {
return Text(
text,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.6,
color: theme.colorScheme.onSurfaceVariant,
),
);
}
Widget _buildConnectionCard(
MeshCoreConnector connector,
BuildContext context,
) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Card(
elevation: 0,
color: colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 24,
backgroundColor: colorScheme.primaryContainer,
child: Icon(
Icons.wifi_tethering_rounded,
color: colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
connector.deviceDisplayName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
connector.deviceIdLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Chip(
avatar: Icon(
Icons.check_circle,
size: 18,
color: colorScheme.onSecondaryContainer,
),
label: Text(context.l10n.common_connected),
backgroundColor: colorScheme.secondaryContainer,
labelStyle: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
visualDensity: VisualDensity.compact,
),
_buildBatteryIndicator(connector, context),
],
),
],
),
),
);
}
Widget _buildQuickSwitchBar(BuildContext context) {
return QuickSwitchBar(
selectedIndex: _quickIndex,
onDestinationSelected: (index) {
_openQuickDestination(index, context);
},
);
}
Widget _buildBatteryIndicator(
MeshCoreConnector connector,
BuildContext context,
) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts;
final percentLabel = percent != null ? '$percent%' : '--%';
final voltageLabel = millivolts == null
? '-- V'
: '${(millivolts / 1000.0).toStringAsFixed(2)} V';
final displayLabel = _showBatteryVoltage ? voltageLabel : percentLabel;
final icon = _batteryIcon(percent);
return ActionChip(
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
label: Text(displayLabel),
labelStyle: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
backgroundColor: colorScheme.secondaryContainer,
visualDensity: VisualDensity.compact,
onPressed: () {
setState(() {
_showBatteryVoltage = !_showBatteryVoltage;
});
},
);
}
IconData _batteryIcon(int? percent) {
if (percent == null) return Icons.battery_unknown;
if (percent <= 15) return Icons.battery_alert;
return Icons.battery_full;
}
void _openQuickDestination(int index, BuildContext context) {
if (_quickIndex != index) {
setState(() {
_quickIndex = index;
});
}
switch (index) {
case 0:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
);
break;
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
}
Future<void> _disconnect(
BuildContext context,
MeshCoreConnector connector,
) async {
await showDisconnectDialog(context, connector);
}
}
+481
View File
@@ -0,0 +1,481 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../utils/contact_search.dart';
import '../utils/platform_info.dart';
import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart';
enum DiscoverySortOption { lastSeen, name, type }
class DiscoveryScreen extends StatefulWidget {
const DiscoveryScreen({super.key});
@override
State<DiscoveryScreen> createState() => _DiscoveryScreenState();
}
class _DiscoveryScreenState extends State<DiscoveryScreen> {
final TextEditingController _searchController = TextEditingController();
String searchQuery = '';
ContactSortOption sortOption = ContactSortOption.lastSeen;
bool showUnreadOnly = false;
ContactTypeFilter typeFilter = ContactTypeFilter.all;
DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen;
Timer? _searchDebounce;
@override
void dispose() {
_searchController.dispose();
_searchDebounce?.cancel();
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;
final connector = context.watch<MeshCoreConnector>();
final discoveredContacts = connector.discoveredContacts;
final filteredAndSorted = _filterAndSortContacts(
discoveredContacts,
connector,
);
return Scaffold(
appBar: AppBar(
title: AppBarTitle(
l10n.discoveredContacts_Title,
indicators: false,
subtitle: false,
),
centerTitle: true,
actions: [
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.discoveredContacts_deleteContactAll),
],
),
onTap: () {
_deleteContacts(context, connector);
},
),
],
icon: const Icon(Icons.more_vert),
),
],
),
body: Column(
children: [
_buildFilters(filteredAndSorted, connector),
Expanded(
child: discoveredContacts.isEmpty
? Center(child: Text(l10n.contacts_noContacts))
: filteredAndSorted.isEmpty
? Center(child: Text(l10n.discoveredContacts_noMatching))
: ListView.builder(
itemCount: filteredAndSorted.length,
itemBuilder: (context, index) {
final contact = filteredAndSorted[index];
final tile = ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
child: Icon(
_getTypeIcon(contact.type),
color: Colors.white,
size: 20,
),
),
title: Text(
contact.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
contact.shortPubKeyHex,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
// 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: () {
connector.importDiscoveredContact(contact);
},
onLongPress: () =>
_showContactContextMenu(contact, connector),
);
if (PlatformInfo.isDesktop) {
return GestureDetector(
onSecondaryTapUp: (_) =>
_showContactContextMenu(contact, connector),
child: tile,
);
}
return tile;
},
),
),
],
),
);
}
Future<void> _showContactContextMenu(
Contact contact,
MeshCoreConnector connector,
) async {
final action = await showModalBottomSheet<String>(
context: context,
showDragHandle: true,
builder: (sheetContext) {
final l10n = context.l10n;
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add_reaction_sharp),
title: Text(l10n.discoveredContacts_addContact),
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(l10n.discoveredContacts_copyContact),
onTap: () => Navigator.of(sheetContext).pop('copy_contact'),
),
ListTile(
leading: const Icon(Icons.delete),
title: Text(l10n.discoveredContacts_deleteContact),
onTap: () => Navigator.of(sheetContext).pop('delete_contact'),
),
],
),
);
},
);
if (!mounted || action == null) return;
switch (action) {
case 'import_contact':
connector.importDiscoveredContact(contact);
break;
case 'copy_contact':
if (contact.rawPacket == null) return;
final hexString = pubKeyToHex(contact.rawPacket!);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
);
break;
case 'delete_contact':
connector.removeDiscoveredContact(contact);
break;
}
}
void _deleteContacts(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.common_deleteAll),
content: Text(l10n.discoveredContacts_deleteContactAllContent),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
connector.removeAllDiscoveredContacts();
},
child: Text(l10n.common_deleteAll),
),
],
),
);
}
Widget _buildFilters(
List<Contact> filteredAndSorted,
MeshCoreConnector connector,
) {
String hintText = "";
switch (typeFilter) {
case ContactTypeFilter.all:
hintText = context.l10n.contacts_searchContacts(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.users:
hintText = context.l10n.contacts_searchUsers(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.repeaters:
hintText = context.l10n.contacts_searchRepeaters(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.rooms:
hintText = context.l10n.contacts_searchRoomServers(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.favorites:
hintText = context.l10n.contacts_searchFavorites(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
}
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: hintText,
prefixIcon: const Icon(Icons.search),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (searchQuery.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
searchQuery = '';
});
},
),
_buildFilterButton(context, connector),
],
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
if (!mounted) return;
setState(() {
searchQuery = value.toLowerCase();
});
});
},
),
),
],
);
}
Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) {
return DiscoveryContactsFilterMenu(
sortOption: sortOption,
typeFilter: typeFilter,
onSortChanged: (value) {
setState(() {
sortOption = value;
});
},
onTypeFilterChanged: (value) {
setState(() {
typeFilter = value;
});
},
);
}
List<Contact> _filterAndSortContacts(
List<Contact> contacts,
MeshCoreConnector connector,
) {
var filtered = contacts.where((contact) {
if (searchQuery.isEmpty) return true;
return matchesDiscoveryContactQuery(contact, searchQuery);
}).toList();
filtered = filtered.where((contact) {
return !connector.knownContactKeys.contains(contact.publicKeyHex);
}).toList();
// Filter out own node from the list
if (connector.selfPublicKey != null) {
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
filtered = filtered.where((contact) {
return contact.publicKeyHex != selfPubKeyHex;
}).toList();
}
if (typeFilter != ContactTypeFilter.all) {
filtered = filtered.where(_matchesTypeFilter).toList();
}
switch (sortOption) {
case ContactSortOption.lastSeen:
filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
break;
case ContactSortOption.name:
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
break;
default:
break;
}
return filtered;
}
bool _matchesTypeFilter(Contact contact) {
switch (typeFilter) {
case ContactTypeFilter.all:
return true;
case ContactTypeFilter.users:
return contact.type == advTypeChat;
case ContactTypeFilter.repeaters:
return contact.type == advTypeRepeater;
case ContactTypeFilter.rooms:
return contact.type == advTypeRoom;
default:
return false;
}
}
IconData _getTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color _getTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
}
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
final now = DateTime.now();
final diff = now.difference(lastSeen);
if (diff.isNegative || diff.inMinutes < 5) {
return context.l10n.contacts_lastSeenNow;
}
if (diff.inMinutes < 60) {
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return hours == 1
? context.l10n.contacts_lastSeenHourAgo
: context.l10n.contacts_lastSeenHoursAgo(hours);
}
final days = diff.inDays;
return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
}
}
+640 -84
View File
@@ -1,6 +1,8 @@
import 'dart:collection';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
@@ -15,6 +17,7 @@ import '../models/app_settings.dart';
import '../models/channel.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
import '../services/path_history_service.dart';
import '../services/map_marker_service.dart';
import '../services/map_tile_cache_service.dart';
import '../utils/contact_search.dart';
@@ -49,7 +52,8 @@ class MapScreen extends StatefulWidget {
}
class _MapScreenState extends State<MapScreen> {
static const double _labelZoomThreshold = 8.5;
// Zoom level at which node labels start to appear
static const double _labelZoomThreshold = 14.0;
final MapController _mapController = MapController();
final MapMarkerService _markerService = MapMarkerService();
@@ -60,10 +64,13 @@ 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;
bool _showNodeLabels = true;
List<_GuessedLocation> _cachedGuessedLocations = [];
String _guessedLocationsCacheKey = '';
@override
void initState() {
@@ -88,6 +95,15 @@ class _MapScreenState extends State<MapScreen> {
});
}
bool _checkLocationPlausibility(double lat, double lon) {
const double epsilon = 1e-6;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
double _standardDeviation(List<double> values) {
if (values.length <= 1) {
return 0.0;
@@ -119,11 +135,16 @@ class _MapScreenState extends State<MapScreen> {
@override
Widget build(BuildContext context) {
return Consumer2<MeshCoreConnector, AppSettingsService>(
builder: (context, connector, settingsService, child) {
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
builder: (context, connector, settingsService, pathHistory, child) {
final tileCache = context.read<MapTileCacheService>();
final settings = settingsService.settings;
final contacts = connector.contacts;
final allContacts = connector.allContacts;
final contacts = settings.mapShowDiscoveryContacts
? allContacts
: allContacts.where((c) => c.isActive).toList();
final highlightPosition = widget.highlightPosition;
final sharedMarkers = settings.mapShowMarkers
? _collectSharedMarkers(connector)
@@ -156,10 +177,44 @@ class _MapScreenState extends State<MapScreen> {
: filteredByTime;
// Filter by location
final contactsWithLocation = filteredByKeyPrefix
final contactsWithLocation = filteredByKeyPrefix.where((c) {
return c.hasLocation;
}).toList();
// All contacts with a known location used as anchors regardless of
// time/key-prefix filters so that repeaters are always available.
final allContactsWithLocation = allContacts
.where((c) => c.hasLocation)
.toList();
// Compute guessed locations with caching
final maxRangeKm = _estimateLoRaRangeKm(connector);
final filteredKeys = filteredByKeyPrefix
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
.join(',');
final anchorKeys = allContactsWithLocation
.map(
(c) =>
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}',
)
.join(',');
final cacheKey =
'$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
if (cacheKey != _guessedLocationsCacheKey) {
_guessedLocationsCacheKey = cacheKey;
_cachedGuessedLocations = settings.mapShowGuessedLocations
? _computeGuessedLocations(
filteredByKeyPrefix,
allContactsWithLocation,
pathHistory,
maxRangeKm,
)
: [];
}
final guessedLocations = settings.mapShowGuessedLocations
? _cachedGuessedLocations
: <_GuessedLocation>[];
_polylines.clear();
_polylines.addAll(
_points.length > 1
@@ -276,7 +331,9 @@ class _MapScreenState extends State<MapScreen> {
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(),
onPressed: () => _startPath(
LatLng(connector.selfLatitude!, connector.selfLongitude!),
),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
@@ -424,12 +481,19 @@ class _MapScreenState extends State<MapScreen> {
point: highlightPosition,
width: 40,
height: 40,
child: Icon(
Icons.location_on_outlined,
color: Colors.red[600],
size: 34,
child: IgnorePointer(
child: Icon(
Icons.location_on_outlined,
color: Colors.red[600],
size: 34,
),
),
),
if (!settings.mapShowOverlaps)
..._buildGuessedMarker(
guessedLocations,
showLabels: _showNodeLabels,
),
..._buildMarkers(
contactsWithLocation,
settings,
@@ -445,28 +509,33 @@ class _MapScreenState extends State<MapScreen> {
),
width: 40,
height: 40,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
child: IgnorePointer(
ignoring: true,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
],
),
alignment: Alignment.center,
child: const Icon(
Icons.person_pin_circle,
color: Colors.white,
size: 20,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(
alpha: 0.3,
),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: const Icon(
Icons.person_pin_circle,
color: Colors.white,
size: 20,
),
),
),
),
@@ -486,9 +555,11 @@ class _MapScreenState extends State<MapScreen> {
),
if (!_isBuildingPathTrace)
_buildLegend(
contacts,
contactsWithLocation,
settings,
sharedMarkers.length,
guessedLocations.length,
),
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
],
@@ -512,31 +583,341 @@ class _MapScreenState extends State<MapScreen> {
);
}
List<_GuessedLocation> _computeGuessedLocations(
List<Contact> allContacts,
List<Contact> withLocation,
PathHistoryService pathHistory,
double? maxRangeKm,
) {
// Index known-location repeaters by their 1-byte hash.
// null value = two repeaters share the same hash byte (ambiguous collision).
final repeaterByHash = <int, Contact?>{};
for (final c in withLocation) {
if (c.type == advTypeRepeater) {
if (repeaterByHash.containsKey(c.publicKey[0])) {
repeaterByHash[c.publicKey[0]] =
null; // collision: can't disambiguate
} else {
repeaterByHash[c.publicKey[0]] = c;
}
}
}
final result = <_GuessedLocation>[];
for (final contact in allContacts) {
if (contact.hasLocation) continue;
if (contact.lastSeen.isBefore(
DateTime.now().subtract(const Duration(hours: 24)),
)) {
continue; // skip stale contacts
}
final anchorSet = <LatLng>{};
// Collect the contact-side (last-hop) repeater from every known path.
// path = [device-side hop, ..., contact-side hop]
// Only path.last is actually within radio range of the contact using
// earlier bytes would anchor against our own side of the network.
final pathSets = <List<int>>[
contact.path.toList(),
...pathHistory
.getRecentPaths(contact.publicKeyHex)
.map((r) => r.pathBytes),
];
final lastHopBytes = <int>{};
for (final pathBytes in pathSets) {
if (pathBytes.isEmpty) continue;
final lastHop = pathBytes.last;
lastHopBytes.add(lastHop);
final r = repeaterByHash[lastHop];
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
}
// Filter anchors that are geometrically inconsistent with radio range.
// Two anchors more than 2 * maxRange apart cannot both be in direct radio
// range of the same node, so isolated outliers are removed.
final anchors = maxRangeKm != null && anchorSet.length > 1
? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm)
: anchorSet.toList();
if (anchors.isEmpty) continue;
final LatLng position;
if (anchors.length == 1) {
// Spread single-anchor guesses around the anchor so they remain visible.
position = _offsetGuessedPosition(
anchors[0],
contact,
radiusMeters: 330,
);
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
)) {
continue; // discard implausible guesses near (0, 0)
}
} else {
double lat = 0, lon = 0, weight = 1.0;
int counted = 0;
for (final a in anchors) {
if (counted == 0) {
lat = a.latitude;
lon = a.longitude;
} else {
lat += a.latitude * weight;
lon += a.longitude * weight;
}
// weight subsequent anchors less to create a bias towards the first (if more than 2)
weight = weight / 2;
counted++;
}
position = _offsetGuessedPosition(
LatLng(lat / anchors.length, lon / anchors.length),
contact,
radiusMeters: anchors.length >= 3 ? 80 : 120,
);
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
)) {
continue; // discard implausible guesses near (0, 0
}
}
result.add(
_GuessedLocation(
contact: contact,
position: position,
highConfidence: anchors.length >= 2,
),
);
}
return result;
}
LatLng _offsetGuessedPosition(
LatLng anchor,
Contact contact, {
required double radiusMeters,
}) {
final seed = _guessSeed(contact.publicKey);
final angle = ((seed & 0xFFFF) / 0x10000) * 2 * pi;
final latOffsetDeg = (radiusMeters / 111320.0) * cos(angle);
final lonScale = max(cos(anchor.latitude * pi / 180.0).abs(), 0.2);
final lonOffsetDeg = (radiusMeters / (111320.0 * lonScale)) * sin(angle);
return LatLng(
anchor.latitude + latOffsetDeg,
anchor.longitude + lonOffsetDeg,
);
}
int _guessSeed(Uint8List publicKey) {
var seed = 0x811C9DC5;
for (final byte in publicKey) {
seed ^= byte;
seed = (seed * 0x01000193) & 0x7FFFFFFF;
}
return seed;
}
/// Estimates the free-space maximum LoRa range in km from the connected
/// device's current radio parameters. Returns null if parameters are unknown.
double? _estimateLoRaRangeKm(MeshCoreConnector connector) {
final freqHz = connector.currentFreqHz;
final bwHz = connector.currentBwHz;
final sf = connector.currentSf;
final txPower = connector.currentTxPower;
if (freqHz == null || bwHz == null || sf == null || txPower == null) {
return null;
}
// LoRa receiver sensitivity = thermal noise + NF + required demod SNR
const noiseFigureDb = 6.0;
final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10;
final sensitivityDbm =
thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf);
// FSPL at max range equals link budget:
// FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55
final linkBudgetDb = txPower.toDouble() - sensitivityDbm;
final exponent =
(linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20;
return pow(10, exponent) / 1000;
}
double _sfToRequiredSnrDb(int sf) {
switch (sf) {
case 5:
return -2.5;
case 6:
return -5.0;
case 7:
return -7.5;
case 8:
return -10.0;
case 9:
return -12.5;
case 10:
return -15.0;
case 11:
return -17.5;
case 12:
return -20.0;
default:
return -10.0;
}
}
/// Removes anchors that have no neighbour within 2 * maxRangeKm.
/// A node cannot be simultaneously in radio range of two points farther apart
/// than twice the expected maximum range.
List<LatLng> _filterConsistentAnchors(
List<LatLng> anchors,
double maxRangeKm,
) {
const distance = Distance();
final maxDistM = maxRangeKm * 2000;
return anchors
.where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM))
.toList();
}
List<Marker> _buildGuessedMarker(
List<_GuessedLocation> guessed, {
required bool showLabels,
}) {
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(
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(
color: color.withValues(
alpha: guess.highConfidence ? 0.55 : 0.30,
),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.not_listed_location,
color: Colors.white,
size: 20,
),
),
),
);
markers.add(marker);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: guess.position,
label: guess.contact.name,
),
);
}
}
return markers;
}
List<Contact> _filterContactsBySettings(
List<Contact> contacts,
dynamic settings, {
bool noLocations = false,
}) {
List<Contact> filtered = [];
bool addContact = false;
for (final contact in contacts) {
addContact = false;
if (!contact.hasLocation && !noLocations) {
continue;
}
// Apply node type filters
if (contact.type == advTypeRepeater &&
(settings.mapShowRepeaters ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}
if (contact.type == advTypeChat &&
(settings.mapShowChatNodes || _isBuildingPathTrace)) {
addContact = true;
}
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(settings.mapShowOtherNodes ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}
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);
}
}
return filtered;
}
List<Marker> _buildMarkers(
List<Contact> contacts,
settings, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final contact in contacts) {
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater &&
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
continue;
}
if (contact.type == advTypeChat &&
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
continue;
}
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
continue;
}
final filteredContacts = _filterContactsBySettings(contacts, settings);
for (final contact in filteredContacts) {
final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 35,
@@ -552,7 +933,9 @@ class _MapScreenState extends State<MapScreen> {
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: _getNodeColor(contact.type),
color: settings.mapShowOverlaps && !_isBuildingPathTrace
? Colors.red
: _getNodeColor(contact.type),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
@@ -579,7 +962,9 @@ class _MapScreenState extends State<MapScreen> {
markers.add(
_buildNodeLabelMarker(
point: LatLng(contact.latitude!, contact.longitude!),
label: contact.name,
label: settings.mapShowOverlaps && !_isBuildingPathTrace
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
: contact.name,
),
);
}
@@ -654,24 +1039,25 @@ class _MapScreenState extends State<MapScreen> {
}
Widget _buildLegend(
List<Contact> contacts,
List<Contact> contactsWithLocation,
settings,
int markerCount,
int guessedCount,
) {
int nodeCount = 0;
for (final contact in contactsWithLocation) {
// Apply node type filters
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
!settings.mapShowOtherNodes) {
continue;
}
nodeCount++;
}
final filteredContacts = _filterContactsBySettings(
contacts,
settings,
noLocations: false,
);
final filteredContactsAll = _filterContactsBySettings(
contacts,
settings,
noLocations: true,
);
final nodeCount = filteredContacts.length;
final nodeCountAll = filteredContactsAll.length;
return Positioned(
top: 16,
@@ -696,12 +1082,65 @@ class _MapScreenState extends State<MapScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.map_nodesCount(nodeCount),
context.l10n.map_nodesCount(
nodeCount +
(settings.mapShowGuessedLocations
? guessedCount
: 0),
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Row(
children: [
Icon(
Icons.location_on,
size: 16,
color: Colors.grey,
),
Text(
": $nodeCount",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
Row(
children: [
const Icon(
Icons.wrong_location,
size: 16,
color: Colors.grey,
),
Text(
": ${nodeCountAll - nodeCount}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
Row(
children: [
const Icon(
Icons.add_outlined,
size: 16,
color: Colors.grey,
),
Text(
": $nodeCountAll",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
Text(
context.l10n.map_pinsCount(markerCount),
style: const TextStyle(
@@ -764,6 +1203,12 @@ class _MapScreenState extends State<MapScreen> {
context.l10n.map_pinPublic,
Colors.orange,
),
if (settings.mapShowGuessedLocations && guessedCount > 0)
_buildLegendItem(
Icons.not_listed_location,
context.l10n.map_guessedLocation,
Colors.grey,
),
],
),
),
@@ -952,7 +1397,12 @@ class _MapScreenState extends State<MapScreen> {
);
}
void _showNodeInfo(BuildContext context, Contact contact) {
void _showNodeInfo(
BuildContext context,
Contact contact, {
LatLng? guessedPosition,
}) {
final connector = context.read<MeshCoreConnector>();
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -972,10 +1422,16 @@ class _MapScreenState extends State<MapScreen> {
children: [
_buildInfoRow('Type', contact.typeLabel),
_buildInfoRow('Path', contact.pathLabel),
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
),
if (contact.hasLocation)
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
)
else if (guessedPosition != null)
_buildInfoRow(
'Est. Location',
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
),
_buildInfoRow(
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
@@ -992,6 +1448,9 @@ class _MapScreenState extends State<MapScreen> {
advTypeChat) // Only show chat button for chat nodes
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
Navigator.push(
context,
@@ -1005,6 +1464,9 @@ class _MapScreenState extends State<MapScreen> {
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
@@ -1013,6 +1475,9 @@ class _MapScreenState extends State<MapScreen> {
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
@@ -1180,6 +1645,23 @@ class _MapScreenState extends State<MapScreen> {
);
},
),
ListTile(
leading: const Icon(Icons.my_location),
title: Text(context.l10n.map_setAsMyLocation),
onTap: () async {
final messenger = ScaffoldMessenger.of(context);
final successMsg = context.l10n.settings_locationUpdated;
Navigator.pop(sheetContext);
if (!connector.isConnected) return;
await connector.setNodeLocation(
lat: position.latitude,
lon: position.longitude,
);
await connector.refreshDeviceInfo();
if (!mounted) return;
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
},
),
ListTile(
leading: const Icon(Icons.close),
title: Text(context.l10n.common_cancel),
@@ -1481,6 +1963,31 @@ class _MapScreenState extends State<MapScreen> {
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showGuessedLocations),
value: settings.mapShowGuessedLocations,
onChanged: (value) {
service.setMapShowGuessedLocations(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showDiscoveryContacts),
value: settings.mapShowDiscoveryContacts,
onChanged: (value) {
service.setMapShowDiscoveryContacts(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showOverlaps),
value: settings.mapShowOverlaps,
onChanged: (value) {
service.setMapShowOverlaps(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
Text(
context.l10n.map_keyPrefix,
@@ -1630,26 +2137,35 @@ 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!));
});
}
void _startPath() {
void _startPath(LatLng position) {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_pathTraceContacts.clear();
_points.clear();
_polylines.clear();
_points.add(position);
});
}
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
@@ -1690,21 +2206,26 @@ class _MapScreenState extends State<MapScreen> {
.join(','),
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
// const SizedBox(height: 6),
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
spacing: 1,
runSpacing: 1,
children: [
if (_pathTrace.isNotEmpty)
ElevatedButton(
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,
),
),
);
@@ -1712,15 +2233,37 @@ class _MapScreenState extends State<MapScreen> {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
tooltip: l10n.map_runTrace,
icon: const Icon(Icons.arrow_forward_outlined),
),
if (_pathTrace.isNotEmpty)
ElevatedButton(
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
flipPathAround: true,
),
),
);
setState(() {
_isBuildingPathTrace = false;
});
},
tooltip: l10n.map_runTraceWithReturnPath,
icon: const Icon(Icons.replay),
),
if (_pathTrace.isNotEmpty)
IconButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
tooltip: l10n.map_removeLast,
icon: const Icon(Icons.undo),
),
if (_pathTrace.isEmpty)
ElevatedButton(
IconButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
@@ -1732,7 +2275,8 @@ class _MapScreenState extends State<MapScreen> {
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
child: Text(l10n.common_cancel),
tooltip: l10n.common_cancel,
icon: const Icon(Icons.close),
),
],
),
@@ -1744,6 +2288,18 @@ class _MapScreenState extends State<MapScreen> {
}
}
class _GuessedLocation {
final Contact contact;
final LatLng position;
final bool highConfidence;
_GuessedLocation({
required this.contact,
required this.position,
required this.highConfidence,
});
}
class _MarkerPayload {
final LatLng position;
final String label;
+20 -10
View File
@@ -44,6 +44,24 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbors;
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) {
if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
);
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
}
@override
void initState() {
super.initState();
@@ -124,12 +142,11 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final contacts = connector.allContacts;
try {
final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) {
for (var neighborData in parsedNeighbors) {
final publicKey = neighborData['publicKey'];
if (listEquals(
@@ -164,13 +181,6 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
}
}
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadNeighbors() async {
if (_commandService == null) return;
+278 -44
View File
@@ -52,16 +52,22 @@ class PathTraceMapScreen extends StatefulWidget {
final String title;
final Uint8List path;
final int? repeaterId;
final bool flipPathRound;
final bool reversePathRound;
final bool flipPathAround;
final bool reversePathAround;
final Contact? targetContact;
final int pathHashByteWidth;
final List<Contact>? pathContacts;
const PathTraceMapScreen({
super.key,
required this.title,
required this.path,
this.repeaterId,
this.flipPathRound = false,
this.reversePathRound = false,
this.flipPathAround = false,
this.reversePathAround = false,
this.targetContact,
this.pathHashByteWidth = pathHashSize,
this.pathContacts,
});
@override
@@ -70,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;
@@ -78,6 +86,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
bool _failed2Loaded = false;
bool _hasData = false;
PathTraceData? _traceData;
// Inferred positions for hops that have no GPS location, keyed by hop byte.
Map<int, LatLng> _inferredHopPositions = {};
// Endpoint position for the target contact (GPS or guessed).
LatLng? _targetContactPosition;
bool _targetContactIsGuessed = false;
List<LatLng> _points = <LatLng>[];
List<Polyline> _polylines = [];
LatLng? _initialCenter = LatLng(0, 0);
@@ -86,6 +99,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
ValueKey<String> _mapKey = const ValueKey('initial');
double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true;
Contact? _targetContact;
String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes
@@ -107,14 +121,42 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
super.dispose();
}
Uint8List addReturnPath(Uint8List pathBytes) {
Uint8List? traceBytes;
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
Uint8List buildPath(Uint8List pathBytes) {
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] = pk?[0] ?? 0;
return traceBytes;
}
if (widget.targetContact?.type == advTypeRepeater ||
widget.targetContact?.type == advTypeRoom) {
final len = (pathBytes.length + pathBytes.length + 1);
traceBytes = Uint8List(len);
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
} else {
if (pathBytes.length < 2) {
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
}
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
}
return traceBytes;
@@ -128,17 +170,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
});
}
final Uint8List path;
Uint8List pathTmp = widget.reversePathRound
final pathTmp = widget.reversePathAround
? Uint8List.fromList(widget.path.reversed.toList())
: widget.path;
if (widget.flipPathRound) {
path = addReturnPath(pathTmp);
} else {
path = pathTmp;
}
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
appLogger.info(
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
tag: 'PathTraceMapScreen',
noNotify: !mounted,
);
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
@@ -228,37 +270,163 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toList();
Map<int, Contact> pathContacts = {};
connector.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.
final Map<int, LatLng> inferredPositions = {};
for (final hop in pathData) {
final contact = pathContacts[hop];
if (contact != null && contact.hasLocation) continue;
final peers = connector.contacts
.where(
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
)
.toList();
if (peers.isNotEmpty) {
final lat =
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
peers.length;
final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length;
inferredPositions[hop] = LatLng(lat, lon);
}
});
}
setState(() {
_isLoading = false;
_hasData = true;
_inferredHopPositions = inferredPositions;
_traceData = PathTraceData(
pathData: pathData,
snrData: snrData,
pathContacts: pathContacts,
);
// Compute endpoint position for the target contact.
LatLng? targetPos;
bool targetGuessed = false;
_targetContact = widget.targetContact;
if (_targetContact != null) {
final tc = _targetContact!;
if (tc.hasLocation) {
targetPos = LatLng(tc.latitude!, tc.longitude!);
} else if (widget.path.length > 1) {
// Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
// sits in the middle of the symmetric sequence; .last is the local side.
final lastHop = widget.reversePathAround
? widget.path.first
: widget.path.last;
final peers = connector.allContacts
.where(
(c) =>
c.hasLocation &&
c.path.isNotEmpty &&
c.path.last == lastHop,
)
.toList();
if (peers.isNotEmpty) {
final lat =
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
peers.length;
final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length;
const offsetDeg = 0.003;
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
lat + offsetDeg * cos(angle),
lon + offsetDeg * sin(angle),
);
targetGuessed = true;
} else if (inferredPositions.containsKey(lastHop)) {
final lat = inferredPositions[lastHop]!.latitude;
final lon = inferredPositions[lastHop]!.longitude;
const offsetDeg = 0.003;
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
lat + offsetDeg * cos(angle),
lon + offsetDeg * sin(angle),
);
targetGuessed = true;
} else {
// As a last resort, just place it at the same position as the last hop.
final contact = pathContacts[lastHop];
if (contact != null && contact.hasLocation) {
const offsetDeg = 0.003;
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
contact.latitude! + offsetDeg * cos(angle),
contact.longitude! + offsetDeg * sin(angle),
);
targetGuessed = true;
}
}
}
}
_targetContactPosition = targetPos;
_targetContactIsGuessed = targetGuessed;
_points = <LatLng>[];
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
int hopLast = 0;
int hopLastLast = 0;
for (final hop in _traceData!.pathData) {
if (hop == hopLastLast && widget.flipPathAround) {
break; //skip duplicate hops in round-trip paths
}
final contact = _traceData!.pathContacts[hop];
if (contact != null &&
contact.hasLocation &&
contact.latitude != null &&
contact.longitude != null) {
if (contact != null && contact.hasLocation) {
_points.add(LatLng(contact.latitude!, contact.longitude!));
} else {
final inferred = inferredPositions[hop];
if (inferred != null) _points.add(inferred);
}
hopLastLast = hopLast;
hopLast = hop;
}
if (targetPos != null) {
if (_targetContact != null && _targetContact!.type == advTypeChat) {
_points.add(targetPos);
}
}
_polylines = _points.length > 1
@@ -349,7 +517,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
],
),
),
if (_hasData) _buildMapPathTrace(context, tileCache),
if (_hasData)
_buildMapPathTrace(context, tileCache, _targetContact),
if (_points.isEmpty &&
!_hasData &&
!_isLoading &&
@@ -378,12 +547,28 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
List<Marker> _buildHopMarkers(
List<int> pathData, {
required bool showLabels,
required Contact? target,
}) {
final markers = <Marker>[];
int hopLast = 0;
int hopLastLast = 0;
for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop];
if (contact == null || !contact.hasLocation) continue;
final point = LatLng(contact.latitude!, contact.longitude!);
final inferred = _inferredHopPositions[hop];
final hasGps = contact != null && contact.hasLocation;
if (hop == hopLastLast && widget.flipPathAround) {
continue; //skip duplicate hops in round-trip paths
}
if (!hasGps && inferred == null) {
hopLastLast = hopLast;
hopLast = hop;
continue; //skip hops with no GPS and no inferred position
}
final point = hasGps
? LatLng(contact.latitude!, contact.longitude!)
: inferred!;
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
markers.add(
Marker(
point: point,
@@ -392,7 +577,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.green,
color: hasGps
? Colors.green
: Colors.orange.withValues(alpha: 0.75),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
@@ -405,10 +592,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
alignment: Alignment.center,
child: Text(
contact.publicKey
.sublist(0, 1)
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(),
hasGps ? label : '~$label',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
@@ -419,8 +603,15 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
);
if (showLabels) {
markers.add(_buildNodeLabelMarker(point: point, label: contact.name));
markers.add(
_buildNodeLabelMarker(
point: point,
label: contact?.name ?? '~$label',
),
);
}
hopLastLast = hopLast;
hopLast = hop;
}
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
@@ -468,6 +659,47 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}
}
// Add target contact endpoint marker.
final targetPos = _targetContactPosition;
if (targetPos != null && target != null && target.type == advTypeChat) {
final isGuessed = _targetContactIsGuessed;
final targetName = target.name;
markers.add(
Marker(
point: targetPos,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isGuessed
? Colors.purple.withValues(alpha: 0.55)
: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: const Icon(Icons.person, color: Colors.white, size: 18),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: targetPos,
label: isGuessed ? '~$targetName' : targetName,
),
);
}
}
return markers;
}
@@ -567,6 +799,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Widget _buildMapPathTrace(
BuildContext context,
MapTileCacheService tileCache,
Contact? target,
) {
return FlutterMap(
key: _mapKey,
@@ -605,6 +838,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers: _buildHopMarkers(
_traceData!.pathData,
showLabels: _showNodeLabels,
target: target,
),
),
],
+20 -3
View File
@@ -35,13 +35,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
@@ -77,11 +79,22 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
});
}
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
}
void _handleTextMessageResponse(Uint8List frame) {
@@ -396,6 +409,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;
}
+1 -2
View File
@@ -205,8 +205,7 @@ class RepeaterHubScreen extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TelemetryScreen(repeater: repeater, password: password),
builder: (context) => TelemetryScreen(contact: repeater),
),
);
},
+13 -2
View File
@@ -129,11 +129,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_commandService?.handleResponse(widget.repeater, parsed.text);
}
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
}
bool _matchesRepeaterPrefix(Uint8List prefix) {
+13 -2
View File
@@ -91,11 +91,22 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
});
}
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
}
void _handleTextMessageResponse(Uint8List frame) {
+228 -47
View File
@@ -1,15 +1,18 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import '../utils/platform_info.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
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 'contacts_screen.dart';
import 'tcp_screen.dart';
import 'usb_screen.dart';
/// Screen for scanning and connecting to MeshCore devices
class ScannerScreen extends StatefulWidget {
@@ -21,6 +24,7 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false;
late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
@@ -28,12 +32,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_connector = Provider.of<MeshCoreConnector>(context, listen: false);
_connectionListener = () {
if (connector.state == MeshCoreConnectionState.disconnected) {
final isCurrentRoute = ModalRoute.of(context)?.isCurrent ?? true;
if (_connector.state == MeshCoreConnectionState.disconnected) {
_changedNavigation = false;
} else if (connector.state == MeshCoreConnectionState.connected &&
} else if (_connector.state == MeshCoreConnectionState.connected &&
_connector.activeTransport == MeshCoreTransportType.bluetooth &&
isCurrentRoute &&
!_changedNavigation) {
_changedNavigation = true;
if (mounted) {
@@ -44,33 +51,52 @@ class _ScannerScreenState extends State<ScannerScreen> {
}
};
connector.addListener(_connectionListener);
_connector.addListener(_connectionListener);
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
if (mounted) {
setState(() {
_bluetoothState = state;
});
// Cancel scan if Bluetooth turns off while scanning
if (state != BluetoothAdapterState.on) {
unawaited(connector.stopScan());
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
(state) {
if (mounted) {
setState(() {
_bluetoothState = state;
});
// Cancel scan if Bluetooth turns off while scanning
if (state != BluetoothAdapterState.on) {
unawaited(_connector.stopScan());
}
}
}
});
},
onError: (Object e) {
appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen');
},
);
}
@override
void dispose() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.removeListener(_connectionListener);
_connector.removeListener(_connectionListener);
unawaited(_bluetoothStateSubscription.cancel());
if (!_changedNavigation) {
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true));
});
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final canPop = Navigator.of(context).canPop();
return Scaffold(
appBar: AppBar(
leading: canPop
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
appLogger.info('Back button pressed', tag: 'ScannerScreen');
Navigator.of(context).maybePop();
},
)
: null,
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
centerTitle: true,
automaticallyImplyLeading: false,
@@ -95,36 +121,84 @@ class _ScannerScreenState extends State<ScannerScreen> {
},
),
),
floatingActionButton: Consumer<MeshCoreConnector>(
bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isScanning =
connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final usbSupported = PlatformInfo.supportsUsbSerial;
final tcpSupported = !PlatformInfo.isWeb;
return FloatingActionButton.extended(
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
connector.startScan();
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (usbSupported)
FloatingActionButton.extended(
onPressed: () {
appLogger.info(
'USB selected, opening UsbScreen',
tag: 'ScannerScreen',
);
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'scanner_usb_action',
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
if (usbSupported) const SizedBox(width: 12),
if (tcpSupported)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const TcpScreen()),
);
},
heroTag: 'scanner_tcp_action',
icon: const Icon(Icons.lan),
label: Text(context.l10n.connectionChoiceTcpLabel),
),
if (tcpSupported) const SizedBox(width: 12),
FloatingActionButton.extended(
heroTag: 'scanner_ble_action',
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
appLogger.warn(
'startScan error: $e',
tag: 'ScannerScreen',
);
}),
);
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
),
],
),
),
);
},
@@ -215,12 +289,33 @@ 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(
@@ -232,6 +327,92 @@ class _ScannerScreenState extends State<ScannerScreen> {
}
}
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(
@@ -265,7 +446,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
],
),
),
if (Platform.isAndroid)
if (PlatformInfo.isAndroid)
TextButton(
onPressed: () => FlutterBluePlus.turnOn(),
child: Text(context.l10n.scanner_enableBluetooth),
+281 -60
View File
@@ -8,10 +8,11 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/radio_settings.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/app_bar.dart';
import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
import '../widgets/radio_stats_entry.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@@ -43,8 +44,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(l10n.settings_title),
centerTitle: true,
title: AppBarTitle(
l10n.settings_title,
indicators: false,
subtitle: false,
),
),
body: SafeArea(
top: false,
@@ -266,6 +270,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),
@@ -275,11 +289,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.visibility_off_outlined),
title: Text(l10n.settings_privacyMode),
subtitle: Text(l10n.settings_privacyModeSubtitle),
leading: const Icon(Icons.group_add_outlined),
title: Text(l10n.settings_contactSettings),
subtitle: Text(l10n.settings_contactSettingsSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => _togglePrivacy(context, connector),
onTap: () => _editAutoAddConfig(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.visibility_off_outlined),
title: Text(l10n.settings_privacy),
subtitle: Text(l10n.settings_privacySubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => _privacySettings(context, connector),
),
],
),
@@ -300,10 +322,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
leading: const Icon(Icons.cell_tower),
title: Text(l10n.settings_sendAdvertisement),
subtitle: Text(l10n.settings_sendAdvertisementSubtitle),
onTap: () => _sendAdvert(context, connector),
leading: const Icon(Icons.delete_outline, color: Colors.red),
title: Text("Delete All Paths"),
subtitle: Text(
"Clear all path data from contacts.",
style: TextStyle(color: Colors.red[700]),
),
onTap: () => connector.deleteAllPaths(),
),
const Divider(height: 1),
ListTile(
@@ -646,55 +671,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
void _togglePrivacy(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_privacyMode),
content: Text(l10n.settings_privacyModeToggle),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await connector.setPrivacyMode(true);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_privacyModeEnabled)),
);
},
child: Text(l10n.common_enable),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await connector.setPrivacyMode(false);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_privacyModeDisabled)),
);
},
child: Text(l10n.common_disable),
),
],
),
);
}
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.sendSelfAdvert(flood: true);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
}
void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.syncTime();
@@ -849,6 +825,251 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
);
}
void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
bool autoAddChat = false;
bool autoAddRepeater = false;
bool autoAddRoomServer = false;
bool autoAddSensor = false;
bool overwriteOldest = false;
final connector = context.read<MeshCoreConnector>();
autoAddChat = connector.autoAddUsers ?? false;
autoAddRepeater = connector.autoAddRepeaters ?? false;
autoAddRoomServer = connector.autoAddRoomServers ?? false;
autoAddSensor = connector.autoAddSensors ?? false;
overwriteOldest = connector.autoAddOverwriteOldest ?? false;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.contactsSettings_autoAddTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FeatureToggleRow(
title: l10n.contactsSettings_autoAddUsersTitle,
subtitle: l10n.contactsSettings_autoAddUsersSubtitle,
value: autoAddChat,
onChanged: (value) {
setDialogState(() => autoAddChat = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRepeatersTitle,
subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle,
value: autoAddRepeater,
onChanged: (value) {
setDialogState(() => autoAddRepeater = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRoomServersTitle,
subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle,
value: autoAddRoomServer,
onChanged: (value) {
setDialogState(() => autoAddRoomServer = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddSensorsTitle,
subtitle: l10n.contactsSettings_autoAddSensorsSubtitle,
value: autoAddSensor,
onChanged: (value) {
setDialogState(() => autoAddSensor = value);
},
),
Divider(height: 4),
FeatureToggleRow(
title: l10n.contactsSettings_overwriteOldestTitle,
subtitle: l10n.contactsSettings_overwriteOldestSubtitle,
value: overwriteOldest,
onChanged: (value) {
setDialogState(() => overwriteOldest = value);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () {
_sendSettings(
connector,
autoAddChat,
autoAddRepeater,
autoAddRoomServer,
autoAddSensor,
overwriteOldest,
);
Navigator.pop(context);
},
child: Text(l10n.common_save),
),
],
),
),
);
}
void _sendSettings(
MeshCoreConnector connector,
bool autoAddChat,
bool autoAddRepeater,
bool autoAddRoomServer,
bool autoAddSensor,
bool overwriteOldest,
) async {
final frame = buildSetAutoAddConfigFrame(
autoAddChat: autoAddChat,
autoAddRepeater: autoAddRepeater,
autoAddRoomServer: autoAddRoomServer,
autoAddSensor: autoAddSensor,
overwriteOldest: overwriteOldest,
);
await connector.sendFrame(frame);
await connector.sendFrame(buildGetAutoAddFlagsFrame());
}
}
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
int telemetryMode = connector.telemetryModeBase;
int telemetryLocMode = connector.telemetryModeLoc;
int telemetryEnvMode = connector.telemetryModeEnv;
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
int multiAcks = connector.multiAcks;
final telemModeBase = [
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
DropdownMenuItem(
value: teleModeAllowFlags,
child: Text(l10n.settings_allowByContact),
),
DropdownMenuItem(
value: teleModeAllowAll,
child: Text(l10n.settings_allowAll),
),
];
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.settings_privacy),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.settings_privacySettingsDescription),
const SizedBox(height: 16),
FeatureToggleRow(
title: l10n.settings_advertLocation,
subtitle: l10n.settings_advertLocationSubtitle,
value: advertLocPolicy,
onChanged: (value) {
setDialogState(() => advertLocPolicy = value);
},
),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
initialValue: telemetryMode,
decoration: InputDecoration(
labelText: l10n.settings_telemetryBaseMode,
border: const OutlineInputBorder(),
),
items: telemModeBase,
onChanged: (value) {
if (value != null) {
setDialogState(() => telemetryMode = value);
}
},
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
initialValue: telemetryLocMode,
decoration: InputDecoration(
labelText: l10n.settings_telemetryLocationMode,
border: const OutlineInputBorder(),
),
items: telemModeBase,
onChanged: (value) {
if (value != null) {
setDialogState(() => telemetryLocMode = value);
}
},
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
initialValue: telemetryEnvMode,
decoration: InputDecoration(
labelText: l10n.settings_telemetryEnvironmentMode,
border: const OutlineInputBorder(),
),
items: telemModeBase,
onChanged: (value) {
if (value != null) {
setDialogState(() => telemetryEnvMode = value);
}
},
),
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());
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await connector.setTelemetryModeBase(
telemetryMode,
telemetryLocMode,
telemetryEnvMode,
advertLocPolicy ? 1 : 0,
multiAcks,
);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
),
);
}
class _RadioSettingsDialog extends StatefulWidget {
+295
View File
@@ -0,0 +1,295 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../utils/platform_info.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'contacts_screen.dart';
import 'usb_screen.dart';
class TcpScreen extends StatefulWidget {
const TcpScreen({super.key});
@override
State<TcpScreen> createState() => _TcpScreenState();
}
class _TcpScreenState extends State<TcpScreen> {
late final TextEditingController _hostController;
late final TextEditingController _portController;
late final MeshCoreConnector _connector;
late final VoidCallback _connectionListener;
bool _navigatedToContacts = false;
@override
void initState() {
super.initState();
_hostController = TextEditingController(
text: context.read<AppSettingsService>().settings.tcpServerAddress,
);
_portController = TextEditingController(
text: context.read<AppSettingsService>().settings.tcpServerPort > 0
? context.read<AppSettingsService>().settings.tcpServerPort.toString()
: '',
);
_connector = context.read<MeshCoreConnector>();
_connectionListener = () {
if (!mounted) return;
if (_connector.state == MeshCoreConnectionState.disconnected) {
_navigatedToContacts = false;
}
if (_connector.state == MeshCoreConnectionState.connected &&
_connector.isTcpTransportConnected &&
!_navigatedToContacts) {
context.read<AppSettingsService>().setTcpServerAddress(
_hostController.text,
);
context.read<AppSettingsService>().setTcpServerPort(
int.tryParse(_portController.text) ?? 0,
);
_navigatedToContacts = true;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const ContactsScreen()),
);
}
};
_connector.addListener(_connectionListener);
}
@override
void dispose() {
_hostController.dispose();
_portController.dispose();
_connector.removeListener(_connectionListener);
if (!_navigatedToContacts &&
_connector.activeTransport == MeshCoreTransportType.tcp &&
_connector.state != MeshCoreConnectionState.disconnected) {
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true));
});
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).maybePop(),
),
title: AdaptiveAppBarTitle(context.l10n.tcpScreenTitle),
centerTitle: true,
),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isConnecting =
connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.tcp;
final isButtonDisabled =
isConnecting ||
connector.state == MeshCoreConnectionState.scanning;
return Column(
children: [
_buildStatusBar(context, connector),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _hostController,
decoration: InputDecoration(
labelText: context.l10n.tcpHostLabel,
hintText: context.l10n.tcpHostHint,
border: const OutlineInputBorder(),
),
enabled: !isConnecting,
keyboardType: TextInputType.url,
),
const SizedBox(height: 12),
TextField(
controller: _portController,
decoration: InputDecoration(
labelText: context.l10n.tcpPortLabel,
hintText: context.l10n.tcpPortHint,
border: const OutlineInputBorder(),
),
enabled: !isConnecting,
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
FilledButton.icon(
key: const Key('tcp_connect_button'),
onPressed: isButtonDisabled ? null : _connectTcp,
icon: isConnecting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.lan),
label: Text(
isConnecting
? context.l10n.scanner_connecting
: context.l10n.common_connect,
),
),
],
),
),
],
);
},
),
),
bottomNavigationBar: SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (PlatformInfo.supportsUsbSerial)
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'tcp_usb_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12),
FloatingActionButton.extended(
onPressed: () {
Navigator.of(context).maybePop();
},
heroTag: 'tcp_ble_action',
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
icon: const Icon(Icons.bluetooth),
label: Text(context.l10n.connectionChoiceBluetoothLabel),
),
],
),
),
),
);
}
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
String statusText;
Color statusColor;
if (connector.isTcpTransportConnected) {
statusText = l10n.scanner_connectedTo(
connector.activeTcpEndpoint ?? 'TCP',
);
statusColor = Colors.green;
} else if (connector.state == MeshCoreConnectionState.connecting &&
connector.activeTransport == MeshCoreTransportType.tcp) {
statusText = l10n.tcpStatus_connectingTo(
'${_hostController.text}:${_portController.text}',
);
statusColor = Colors.orange;
} else if (connector.state == MeshCoreConnectionState.disconnecting &&
connector.activeTransport == MeshCoreTransportType.tcp) {
statusText = l10n.scanner_disconnecting;
statusColor = Colors.orange;
} else {
statusText = l10n.tcpStatus_notConnected;
statusColor = Colors.grey;
}
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: statusColor.withValues(alpha: 0.1),
child: Row(
children: [
Icon(Icons.circle, size: 12, color: statusColor),
const SizedBox(width: 8),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}
Future<void> _connectTcp() async {
if (_connector.state == MeshCoreConnectionState.connecting ||
_connector.state == MeshCoreConnectionState.connected ||
_connector.state == MeshCoreConnectionState.disconnecting) {
return;
}
final host = _hostController.text.trim();
final parsedPort = int.tryParse(_portController.text.trim());
if (host.isEmpty) {
_showError(context.l10n.tcpErrorHostRequired);
return;
}
if (parsedPort == null || parsedPort < 1 || parsedPort > 65535) {
_showError(context.l10n.tcpErrorPortInvalid);
return;
}
try {
await _connector.connectTcp(host: host, port: parsedPort);
} catch (error) {
if (!mounted) return;
_showError(_friendlyErrorMessage(error));
}
}
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
}
String _friendlyErrorMessage(Object error) {
if (error is UnsupportedError) {
return context.l10n.tcpErrorUnsupported;
}
if (error is TimeoutException) {
return context.l10n.tcpErrorTimedOut;
}
if (error is StateError) {
return context.l10n.tcpConnectionFailed(error.message);
}
if (error is ArgumentError) {
return context.l10n.tcpConnectionFailed(
error.message?.toString() ?? error.toString(),
);
}
return context.l10n.tcpConnectionFailed(error.toString());
}
}
+95 -73
View File
@@ -10,30 +10,22 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../utils/app_logger.dart';
import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart';
class TelemetryScreen extends StatefulWidget {
final Contact repeater;
final String password;
final Contact contact;
const TelemetryScreen({
super.key,
required this.repeater,
required this.password,
});
const TelemetryScreen({super.key, required this.contact});
@override
State<TelemetryScreen> createState() => _TelemetryScreenState();
}
class _TelemetryScreenState extends State<TelemetryScreen> {
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _tagData = 0;
bool _isLoading = false;
bool _isLoaded = false;
@@ -44,6 +36,26 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedTelemetry;
int _tripTime = 0;
int _resolveContactIndex = -1;
Contact _resolveContact(MeshCoreConnector connector) {
if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
);
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
}
@override
void initState() {
super.initState();
@@ -60,27 +72,62 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final reader = BufferReader(frame);
try {
final cmd = reader.readByte();
if (cmd == respCodeSent) {
reader.skipBytes(1); // Skip the reserved byte
_tagData = reader.readUInt32LE();
_tripTime = reader.readUInt32LE();
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: _tripTime), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
),
);
_recordTelemetryResult(false);
});
}
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
}
// Check if it's a binary response
if (cmd == pushCodeBinaryResponse) {
if (!mounted) return;
reader.skipBytes(1); // Skip the reserved byte
if (reader.readUInt32LE() != _tagData) return;
_handleTelemetryResponse(reader.readRemainingBytes());
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
if (!mounted) return;
_handleStatusResponse(frame.sublist(6));
// Check if it's a telemetry response (for chat contacts)
if (cmd == pushCodeTelemetryResponse) {
reader.skipBytes(1); // Skip the reserved byte
final pubkey = reader.readBytes(6);
if (!mounted) return;
if (!listEquals(widget.contact.publicKey.sublist(0, 6), pubkey)) {
return;
}
_handleTelemetryResponse(reader.readRemainingBytes());
}
} catch (e) {
appLogger.error('Error parsing incoming frame: $e');
// If parsing fails, ignore the frame
}
});
}
void _handleStatusResponse(Uint8List frame) {
void _handleTelemetryResponse(Uint8List frame) {
final parsedTelemetry = CayenneLpp.parseByChannel(frame);
final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
if (batteryMv != null) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.updateRepeaterBatterySnapshot(
widget.repeater.publicKeyHex,
widget.contact.publicKeyHex,
batteryMv,
source: 'telemetry',
);
@@ -105,13 +152,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
});
}
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadTelemetry() async {
if (_commandService == null) return;
@@ -121,41 +161,20 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
});
try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
final selection = await connector.preparePathForContactSend(repeater);
final selection = await connector.preparePathForContactSend(
_resolveContact(connector),
);
_pendingStatusSelection = selection;
final frame = buildSendBinaryReq(
repeater.publicKey,
payload: Uint8List.fromList([reqTypeGetTelemetry]),
);
await connector.sendFrame(frame);
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
var messageBytes = frame.length >= _statusResponseBytes
? frame.length
: _statusResponseBytes;
if (messageBytes < maxFrameSize) {
messageBytes = maxFrameSize;
}
final timeoutMs = connector.calculateTimeout(
pathLength: pathLengthValue,
messageBytes: messageBytes,
);
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
),
Uint8List frame;
if (widget.contact.type != advTypeChat) {
frame = buildSendBinaryReq(
widget.contact.publicKey,
payload: Uint8List.fromList([reqTypeGetTelemetry]),
);
_recordStatusResult(false);
});
} else {
frame = buildSendTelemetryReq(widget.contact.publicKey);
}
await connector.sendFrame(frame);
} catch (e) {
if (mounted) {
setState(() {
@@ -173,12 +192,16 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
}
}
void _recordStatusResult(bool success) {
void _recordTelemetryResult(bool success) {
final selection = _pendingStatusSelection;
if (selection == null) return;
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
connector.recordRepeaterPathResult(repeater, selection, success, null);
connector.recordRepeaterPathResult(
widget.contact,
selection,
success,
null,
);
_pendingStatusSelection = null;
}
@@ -196,8 +219,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
final connector = context.watch<MeshCoreConnector>();
final settings = context.watch<AppSettingsService>().settings;
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
final repeater = _resolveRepeater(connector);
final isFloodMode = repeater.pathOverride == -1;
final isFloodMode = widget.contact.pathOverride == -1;
return Scaffold(
appBar: AppBar(
@@ -210,7 +232,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
repeater.name,
widget.contact.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
@@ -225,9 +247,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
await connector.setPathOverride(widget.contact, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
await connector.setPathOverride(widget.contact, pathLen: null);
}
},
itemBuilder: (context) => [
@@ -283,7 +305,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
PathManagementDialog.show(context, contact: widget.contact),
),
IconButton(
icon: _isLoading
@@ -437,7 +459,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final batteryMv =
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
connector.getRepeaterBatteryMillivolts(widget.contact.publicKeyHex) ??
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
if (batteryMv == null) return l10n.common_notAvailable;
final chemistry = _batteryChemistry();
@@ -449,7 +471,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
String _batteryChemistry() {
final settingsService = context.read<AppSettingsService>();
return settingsService.batteryChemistryForRepeater(
widget.repeater.publicKeyHex,
widget.contact.publicKeyHex,
);
}

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