Compare commits

..

141 Commits

Author SHA1 Message Date
Winston Lowe 0135d56ddc Add localized search functionality for contacts
- 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:39:06 -08:00
zjs81 bdb1eb6b42 Merge pull request #179 from MGJ520/main
Correct Chinese translation
2026-02-24 20:26:54 -07:00
zjs81 f2ccec2926 Merge branch 'main' into MGJ520/main
# Conflicts:
#	lib/l10n/app_zh.arb
2026-02-24 20:21:10 -07:00
zjs81 31671958d5 Merge pull request #234 from ericszimmermann/favorite_filter_ez
favorites handling only
2026-02-24 20:15:05 -07:00
zjs81 ea379ce50b Fix dart format line length in contacts_screen.dart 2026-02-24 20:11:56 -07:00
zjs81 50af2e0bc9 Fix review issues: dedicated l10n keys, remove unrelated CI/macOS changes, translate all locales
- Replace concatenated favorite toggle strings with dedicated listFilter_addToFavorites/removeFromFavorites keys
- Remove unrelated CI artifact upload step from build.yml
- Revert unrelated macOS GeneratedPluginRegistrant.swift change
- Add comment explaining groups hidden under favorites filter
- Translate new keys across all 14 locales
2026-02-24 20:07:15 -07:00
zjs81 d5ac84430c Merge branch 'main' into favorite_filter_ez 2026-02-24 19:51:02 -07:00
zjs81 190fd3b353 Remove pubspec.lock from version control 2026-02-24 19:44:15 -07:00
zjs81 a2d1cb2a99 add pubspec.lock to .gitignore 2026-02-24 19:42:12 -07:00
zjs81 83386a8cde Merge pull request #214 from MeshEnvy/fix/cursor-focus
enh: return cursor focus to message window after send
2026-02-24 19:37:43 -07:00
zjs81 acc0fff2dc Merge pull request #215 from MeshEnvy/fix/enter-send-giphy
enh <enter> to send giphy
2026-02-24 19:13:55 -07:00
ericz a26055c93f resolved analyte code failure: unused import 2026-02-25 00:49:41 +01:00
ericz 5a70ed48cf favorites handling only 2026-02-24 23:56:30 +01:00
zjs81 a42cf77a70 Merge pull request #232 from just-stuff-tm/PR-Combined-228-220-219-201
Pr combined #228 #220 #219 #201
2026-02-24 13:20:02 -07:00
just_stuff_tm 31db565ebf PR Combined #228 #220 #219 #201 2026-02-24 13:20:39 -05:00
just_stuff_tm 515b9c1f29 fix los init localization 2026-02-24 12:51:58 -05:00
just_stuff_tm ea1d728d4f Merge remote-tracking branch 'origin/issue-fix-channel-edit-delete-actions' into combined-prs 2026-02-24 12:45:51 -05:00
just_stuff_tm 86bde1d178 Merge remote-tracking branch 'origin/los-elevation-icon' into combined-prs 2026-02-24 12:45:47 -05:00
just_stuff_tm de63733bb9 Merge remote-tracking branch 'origin/calculate-refrac-los' into combined-prs 2026-02-24 12:40:26 -05:00
just_stuff_tm c880c2d107 fix channel actions context 2026-02-24 00:02:10 -05:00
just_stuff_tm 2a7cc28a3a fix 2026-02-23 23:46:25 -05:00
just_stuff_tm 8a16024642 fix(chat): stabilize pinch-to-zoom scaling 2026-02-23 23:06:27 -05:00
just_stuff_tm 0f17e2382c feat(chat): add global pinch-to-zoom text scaling via ChatTextScaleService 2026-02-23 22:41:32 -05:00
just_stuff_tm 6065059241 fix: keep los panel reactive 2026-02-23 19:35:51 -05:00
just_stuff_tm faefef14ff fix: restore baseline freq in los text 2026-02-23 19:29:36 -05:00
just_stuff_tm ddc87f3a27 chore: remove translation script 2026-02-23 19:14:00 -05:00
just_stuff_tm 2188b49726 fix: refresh los localization 2026-02-23 19:06:52 -05:00
just_stuff_tm 1a9b7b0d55 chore: remove 0.15 text 2026-02-23 18:18:02 -05:00
just_stuff_tm 74e29a6c0f fix: clamp los profile bounds 2026-02-23 18:12:04 -05:00
Ben Allfree 972ae809e3 Merge branch 'main' into fix/cursor-focus 2026-02-23 14:58:03 -08:00
Ben Allfree deb46553f3 Merge remote-tracking branch 'origin/main' into fix/enter-send-giphy 2026-02-23 14:57:28 -08:00
Ben Allfree 58fc55df13 Merge remote-tracking branch 'upstream/main' 2026-02-23 14:56:00 -08:00
just_stuff_tm ea2f35ec2e fix: keep los metadata on failure 2026-02-23 15:59:18 -05:00
just_stuff_tm e2585c0992 fix: reduce rebuilds in los panel 2026-02-23 15:44:21 -05:00
just_stuff_tm 78f1a7b28e fix: normalize stored frequency 2026-02-23 15:12:32 -05:00
just_stuff_tm 0121b5f649 Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-23 14:47:48 -05:00
just_stuff_tm ec14870aed Update after upstream merged other commits 2026-02-23 14:42:30 -05:00
just_stuff_tm c0516a475d fix: extend los profile edges 2026-02-23 14:36:10 -05:00
just_stuff_tm b998186430 Merge branch 'main' into los-elevation-icon 2026-02-23 12:19:57 -05:00
just_stuff_tm 16b2c24983 Propagate LOS frequency data and clamp bounds 2026-02-23 12:18:42 -05:00
just_stuff_tm c8ff0cc943 Merge upstream/main 2026-02-23 12:14:38 -05:00
zjs81 64bf307d09 Merge pull request #216 from MeshEnvy/feat/hide-message-tracing
feat: hide message tracing
2026-02-23 07:16:24 -07:00
Ben Allfree 88f8066ed3 code formatting 2026-02-23 04:53:01 -08:00
Ben Allfree bf5fadd15e revert lockfile 2026-02-23 04:13:52 -08:00
Ben Allfree 3730b2a6c2 formatting 2026-02-23 04:13:38 -08:00
Ben Allfree 173fdf7168 chat fixes 2026-02-23 04:11:46 -08:00
Ben Allfree 7465e81996 add done_all icon 2026-02-23 03:31:01 -08:00
just_stuff_tm 677b25908a Document LOS frequency and k-factor math
Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact
2026-02-23 03:11:14 -05:00
just_stuff_tm fc55fb98ce Document LOS frequency and k-factor math
Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact
2026-02-23 02:48:28 -05:00
just_stuff_tm 2bdd9d35cc feat: show radio horizon on los profile 2026-02-23 02:02:17 -05:00
just_stuff_tm 1f816f7e08 ran dart format . on libs/icons/los_icon.dart 2026-02-23 01:06:25 -05:00
just_stuff_tm bd27c90216 feat: render los elevation via material symbol 2026-02-23 00:54:51 -05:00
just_stuff_tm 9bcb8b9ca6 feat: render los elevation via svg 2026-02-23 00:36:49 -05:00
just_stuff_tm aaf79c90c9 feat: show los elevation icon 2026-02-23 00:01:13 -05:00
just_stuff_tm 08edd2696e Revert "feat: add custom los icon"
This reverts commit 0f2d18d6fa.
2026-02-22 23:47:49 -05:00
just_stuff_tm 0f2d18d6fa feat: add custom los icon 2026-02-22 23:39:52 -05:00
just_stuff_tm 298951f8bc bring up to current main 2026-02-22 18:43:37 -05:00
just_stuff_tm f3db63ceea Delete pubspec.lock 2026-02-22 17:37:58 -05:00
just_stuff_tm 47044ae14e fix(l10n): add channels_channelDeleteFailed with proper placeholder typing and translations 2026-02-22 17:37:10 -05:00
just_stuff_tm f4dd76a459 Delete .local-agent/memory.local.md 2026-02-22 16:07:32 -05:00
just_stuff_tm ab76a52d71 Delete .local-agent/AGENTS.local.md 2026-02-22 16:07:19 -05:00
just_stuff_tm 332bb5ef3a Updated PR and Added snackbar Translations 2026-02-22 16:06:08 -05:00
just_stuff_tm 81a423d096 Merge branch 'main' into issue-fix-channel-edit-delete-actions 2026-02-22 15:49:51 -05:00
zjs81 700e85b13d Merge pull request #208 from Specter242/codex/java17-wakelock-alignment
Align Android app module to Java 17 and bump wakelock_plus
2026-02-22 13:10:49 -07:00
zjs81 9a27953a6e Merge pull request #196 from zjs81/fix-channel-del
clear app db of channel messages on delete
2026-02-22 13:10:05 -07:00
just_stuff_tm abde4a5e46 Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-22 15:06:58 -05:00
zjs81 6e1cb0482f Merge branch 'main' into fix-channel-del 2026-02-22 13:01:36 -07:00
zjs81 c28b38a233 Merge pull request #210 from spfmoby/better-french-translations
Better french translations
2026-02-22 12:58:58 -07:00
zjs81 722caf774e Merge pull request #211 from MeshEnvy/chrome/1-readme
docs: add chrome support notice
2026-02-22 12:56:43 -07:00
Ben Allfree d269e181c3 formatting fix 2026-02-22 11:34:18 -08:00
Ben Allfree 35498c1b90 formatting fix 2026-02-22 11:31:56 -08:00
Ben Allfree bf4f52a4e3 hide message tracing 2026-02-22 11:27:32 -08:00
Ben Allfree 3ca53e967c fix: <enter> to send giphy 2026-02-22 10:51:19 -08:00
Ben Allfree 096e0a4184 fix: return cursor to message window after send 2026-02-22 10:49:28 -08:00
Ben Allfree c7b33f1d1b readme update 2026-02-22 06:51:40 -08:00
Ben Allfree 7288f11c88 add chrome in planning 2026-02-22 06:49:14 -08:00
spfmoby 2306269384 Better french translations 2026-02-22 15:20:55 +01:00
just_stuff_tm 41ff2353a4 Merge branch 'main' into issue-fix-channel-edit-delete-actions 2026-02-22 06:40:04 -05:00
Krasimir Kazakov b3ad54f296 Added mute channel functionality (#209) 2026-02-21 23:51:48 -08:00
Leah 7cb4c5a334 Swipe to reply (#160)
* Add swipe to reply

* format

* Cleaned up code

* format

* remove my gitignore change - ignore this

* fix

* Update lib/screens/channel_chat_screen.dart

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

* Update lib/screens/channel_chat_screen.dart

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

* Refactor onHorizontalDragStart for readability

fixed formating.

* Fix swipe end handling in channel chat screen

* Refactor swipe gesture handling in chat screen

* Update lib/screens/channel_chat_screen.dart

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

* Update lib/screens/channel_chat_screen.dart

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

* Refactor swipe handling for reply functionality

* Adjust swipe thresholds and logic in chat screen

* Conditionally render reply bubble or padding

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Winston Lowe <wel97459@gmail.com>
2026-02-21 23:44:20 -08:00
just_stuff_tm bb8ad70cb9 Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-22 00:04:28 -05:00
Specter242 8fe4129204 Align Android app module to Java 17 and bump wakelock_plus 2026-02-21 21:01:57 -05:00
Aaron Easterling 2feff809ff Mark pending channel messages sent on RESP_CODE_SENT (#186)
* Mark pending channel message sent on RESP_CODE_SENT

* Disambiguate RESP_CODE_SENT handling for direct vs channel

* Handle channel sent feedback when firmware returns RESP_CODE_OK

* Correlate channel OK ACKs and queue reaction channel sends
2026-02-21 15:31:51 -08:00
just_stuff_tm 51d70ce086 fix(appbar): prevent title overflow on narrow widths (#205)
Use width-aware layout in AppBarTitle to avoid RenderFlex overflows under tight title constraints and larger text scaling. Hide subtitle and signal indicators progressively when space is limited while preserving normal behavior on wider layouts.
2026-02-21 15:20:56 -08:00
Winston Lowe b05b62eeee Changed all map lables to look the same across all map ui (#206)
* Refactor label display in Line Of Sight and Map screens for improved alignment and styling

* Refactor label positioning and styling in ChannelMessagePathMap and PathTraceMap screens for improved alignment
2026-02-21 14:55:42 -08:00
just_stuff_tm 061b715694 Fix repeater battery % inconsistency and add configurable repeater battery chemistry (#199)
* fix(repeater): unify battery percentage math and add repeater chemistry setting

- Add shared battery percent utility used by connector, repeater status, and telemetry

- Add repeater-specific battery chemistry persistence and service accessors

- Add repeater chemistry selector in Repeater Hub

- Ensure telemetry and status compute percentages consistently from same chemistry

- Add focused battery utility tests

Refs #116

Refs #174

* fix: Flutter Analyzer Errors fixed Recent Merge Compatible

* Unify repeater battery source across status and telemetry
2026-02-21 14:54:39 -08:00
just_stuff_tm f38b8b0319 Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-21 12:54:06 -05:00
Winston Lowe 304c389669 Refactor label display in Line Of Sight and Map screens for improved alignment and styling (#204) 2026-02-20 23:41:20 -08:00
Winston Lowe 7acfe47fd7 Refactor map legend and filtering logic for contacts with location, to show count of active markers. (#203) 2026-02-20 22:09:11 -08:00
just_stuff_tm f4b18d97a1 Added Line Of Sight Feature for repeater placement, Added app wide Units Setting (#198)
* feat: add LOS workflow, global units, l10n cleanup, and mobile UI overflow fixes

Squashes prior PR commits into one changeset including: LOS map/service/tests, global metric/imperial unit system adoption, notification/BLE safety fixes, app-wide localization backfill/mojibake cleanup, and responsive UI title/overflow hardening.

* l10n: revert unrelated locale churn for LOS feature

* feat: keep LOS with app-wide unit settings

* fix: resolve post-merge app bar/import analyzer errors

* style: format screen files for CI
2026-02-20 22:08:23 -08:00
Winston Lowe d2b693e5ce Add a signal readout for the nearest repeater. With improvements to app bar and other UI polish. (#200)
* Refactor Cayenne LPP parsing with error handling and logging

- Added error handling and logging to the Cayenne LPP parsing methods to manage malformed data gracefully.
- Improved the structure of the parsing logic for better readability and maintainability.
- Updated the Contact model to include error handling during frame parsing.
- Refactored Channels, Contacts, Map, and Neighbours screens to utilize a new AppBarTitle widget for consistent app bar design.
- Enhanced the BatteryIndicator widget to display SNR information for direct repeaters.
- Introduced SNRUi class for better management of SNR icon and text representation.
- Improved error handling in PathTraceMap and Neighbours screens to log errors appropriately.

* Fix trace route bytes generation logic in Contact model

* Ignore advertisements from self in MeshCoreConnector

* Refactor PathTraceData to use List<double> for snrData and adjust data mapping in PathTraceMapScreen

* Add SNRIndicator to AppBar and refactor BatteryIndicator layout

* Enhance path management dialog to display direct repeaters with color coding based on signal strength

* Remove unused import from SNR indicator widget

* Update lib/models/contact.dart

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

* Update lib/connector/meshcore_connector.dart

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

* Update lib/connector/meshcore_connector.dart

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

* Update lib/screens/path_trace_map.dart

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

* Update lib/widgets/battery_indicator.dart

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

* Update lib/helpers/cayenne_lpp.dart

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

* Refactor packet handling to skip only the RSSI byte for improved reliability

* Add SNR indicator localization and update UI references for nearby repeaters

* Handle loading state and error parsing in PathTraceMapScreen; update SNR indicator dialog content layout

* Throw an exception for unsupported LPP types in CayenneLpp class

* Refactor AppBarTitle widget to remove unused style parameter; update related screens to reflect changes
Improve SNR handling by adding validation for spreading factor range in snrUiFromSNR function
Update contact handling in MeshCoreConnector to fix variable naming and improve readability
Stop parsing unsupported LPP types in CayenneLpp to avoid misalignment

* Sort direct repeaters by last updated time and SNR; limit to top three for improved path management dialog

* Prevent notifications for chat and sensor adverts without a valid path

* Implement ranking system for direct repeaters based on SNR and recency; update related UI components to reflect changes

* Refactor localization keys for "neighbors" terminology across multiple languages

- Updated localization keys from "neighbours" to "neighbors" in the following files:
  - app_localizations_bg.dart
  - app_localizations_de.dart
  - app_localizations_en.dart
  - app_localizations_es.dart
  - app_localizations_fr.dart
  - app_localizations_it.dart
  - app_localizations_nl.dart
  - app_localizations_pl.dart
  - app_localizations_pt.dart
  - app_localizations_ru.dart
  - app_localizations_sk.dart
  - app_localizations_sl.dart
  - app_localizations_sv.dart
  - app_localizations_uk.dart
  - app_localizations_zh.dart
- Updated corresponding ARB files to reflect the changes in keys.
- Renamed the NeighboursScreen to NeighborsScreen in the chat and repeater hub screens for consistency.

* Adjust ranking calculation for direct repeaters by adding offset to SNR for improved accuracy

* Fix typo in variable name for second direct repeater in path management dialog

* Refactor ranking calculation for direct repeaters and update path handling in channel message screens

* Refactor path handling in ChannelMessagePathScreen to improve logic for outgoing messages and channel messages

* Fix AppBarTitle horizontal overflow with long titles (#187)

* Initial plan

* Wrap title Column in Expanded to prevent horizontal overflow in AppBarTitle

Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com>

* Refactor AppBarTitle widget to simplify Text widget initialization

* Add "Show All Paths" feature to chat path management

- Implemented localization for "Show All Paths" in multiple languages (DE, EN, ES, FR, IT, NL, PL, PT, RU, SK, SL, SV, UK, ZH).
- Updated path management dialog to include a toggle for showing all paths.
- Refactored path history display logic to conditionally show paths based on the toggle state.
- Cleaned up unused print statements and improved code readability in path tracing and chat screens.

* Refactor FeatureToggleRow visibility in chat and path management dialogs based on repeaters list

* Remove unused import of 'dart:ffi' in path_trace_map.dart

* Refactor repeater management logic and update UI state handling in chat and path management dialogs

* Refactor RX data handling and improve repeater management logic in chat and path management dialogs

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com>
2026-02-20 20:27:38 -08:00
just_stuff_tm ba2763a3f6 fix(channels): make edit/delete actions use parent context after bottom sheet closes
Root cause: edit/delete dialogs were opened from the sheet context after Navigator.pop, so context.mounted was false and follow-up actions never ran.

Also keeps async await/error handling for channel edit/delete so failures surface to users instead of silently dropping.
2026-02-20 01:28:13 -05:00
Ded 0c4910e149 Merge pull request #195 from MeshEnvy/rbenv
add rbenv support
2026-02-19 12:40:27 -08:00
446564 4bf2519559 clear app db of channel messages on delete
we were only deleting channels and messages on device and the in app db would persist
this caused weird messages to later show up in other channels as they were deleted and
added due to the fact we store messages by channel index(slot #)
2026-02-19 11:46:57 -08:00
Ben Allfree 19edeab9d5 add rbenv support 2026-02-19 11:17:58 -08:00
MGJ 0e81d75cce Merge branch 'main' into main 2026-02-19 13:07:08 +08:00
zjs81 9437846127 Merge pull request #182 from Specter242/feature/protocol-compat
Handle RESP_CODE_ERR frames explicitly in connector
2026-02-18 13:04:16 -07:00
Specter242 50ab46ed40 Remove incidental whitespace-only diff from protocol PR 2026-02-18 12:45:41 -05:00
Specter242 dc193be8ed Trim protocol PR to explicit RESP_CODE_ERR handling only 2026-02-18 12:45:02 -05:00
Specter242 8a804a3706 Remove unused protocol placeholder field and unify version source 2026-02-18 12:30:00 -05:00
Specter242 1dc90d0e89 Add device protocol version tracking and error frame handling
Port from meshcore-open: parse protocol version from byte 1 of device
info frame, expose supportsFloodScope getter (version >= 8), handle
respCodeErr frames with debug logging. Reset on disconnect.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit a29bb9cdd7a02a85af26d94dd3c787cabd124629)
2026-02-18 11:14:01 -05:00
zjs81 5f2312e086 Merge pull request #180 from zjs81/fix/radio-params-fw-compat
Refactor radio settings and localization updates
2026-02-18 00:14:32 -07:00
zjs81 4239fb11ed Fix radio settings to only send repeat byte when the current state is known 2026-02-18 00:07:08 -07:00
zjs81 5fae2e5f73 fix formatting 2026-02-17 23:50:11 -07:00
zjs81 947fafbbb7 Refactor radio settings and localization updates
fixes #72

- Removed preset configurations for 915 MHz, 868 MHz, and 433 MHz from the RadioSettings model.
- Introduced a new list of regional preset configurations for various countries.
- Updated the settings screen to use a dropdown for selecting presets instead of chips.
- Added a switch for enabling client repeat functionality with appropriate warnings for frequency usage.
- Updated localization files for multiple languages to reflect changes in settings related to client repeat functionality.
2026-02-17 23:42:04 -07:00
MGJ a9fbf8c7f5 Correct Chinese translation 2026-02-17 13:30:23 +08:00
zjs81 72f0aa7208 Update dependencies and improve code consistency across multiple files 2026-02-14 02:22:45 -07:00
zjs81 f87d4896ab Merge pull request #161 from ChaoticLeah/enhancement/bluetooth-disabled-warning
Add warning when bluetooth is off
2026-02-14 02:00:36 -07:00
zjs81 9250dfec31 Gate the turn on BLE button to android 2026-02-14 01:54:30 -07:00
zjs81 37db955ab2 Fixed banner flash, added enable bluetooth button fixed theme to use app theme colors removed FAB overrides because material 3 does this for us, fixed missing translations. 2026-02-14 01:46:40 -07:00
zjs81 739d9475c0 Merge remote-tracking branch 'origin/main' into enhancement/bluetooth-disabled-warning 2026-02-14 01:28:54 -07:00
zjs81 b526175be4 bump version for android 2026-02-14 01:13:06 -07:00
Winston Lowe 73081862ad Add path tracing functionality (#165)
- Implemented path tracing feature in the map screen, allowing users to add nodes to a path and visualize it on the map.
- Added buttons for starting path tracing, removing the last node, and running the path trace.
- Introduced a new overlay to display current path information and distance.
- Updated localization files for multiple languages to include new strings related to path tracing.
- Refactored map rendering logic to accommodate path tracing visuals.
2026-02-14 01:10:34 -07:00
just_stuff_tm fac062a100 Refine device info layout and add collapsible map legend (#164) 2026-02-12 13:46:28 -07:00
Leah ef6bd78632 Merge branch 'enhancement/bluetooth-disabled-warning' of github.com:ChaoticLeah/meshcore-open into enhancement/bluetooth-disabled-warning 2026-02-12 20:15:58 +01:00
Leah 01c8390989 make stuff unawaited + maybe fix edge case? 2026-02-12 20:14:56 +01:00
Leah c05f813d65 Update lib/screens/scanner_screen.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 20:02:56 +01:00
Ded c52b19b09f Merge pull request #162 from just-stuff-tm/fix/battery-layout-overflow
Fix battery chemistry dropdown layout overflow
2026-02-11 21:27:30 -08:00
just_stuff_tm 6a666839b6 Fix battery chemistry dropdown layout overflow 2026-02-12 00:05:00 -05:00
Leah bc77f7e287 Remove unused translation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 23:03:41 +01:00
Leah 9332d8126f linted and added greying out 2026-02-11 22:58:15 +01:00
Leah 9ce00556ec Add warning when bluetooth is off 2026-02-11 22:40:42 +01:00
Ded 4995f5f380 Merge pull request #159 from ChaoticLeah/add-flake-nix
Add flake.nix for development environment
2026-02-11 09:10:46 -08:00
Leah 4e6e7b6061 fix smaller copilot issues 2026-02-11 17:45:15 +01:00
Leah aa350aa4ae fixing copilot issues 2026-02-11 17:33:31 +01:00
Leah dfd38b19e9 add flake.lock 2026-02-11 17:26:43 +01:00
Leah 4afab3f629 remove unnessisary bits and nix darwin stuff 2026-02-11 17:25:44 +01:00
Ded 67816130ac Merge pull request #152 from zjs81/remove-wakelock
remove wakelock
2026-02-11 08:18:57 -08:00
Ded d573f0c312 Merge pull request #158 from ChaoticLeah/update-gitignore
Update .gitignore to exclude .gradle/
2026-02-11 08:16:38 -08:00
Ded 5b699cd624 keep ignores organized 2026-02-11 08:16:07 -08:00
Leah a4d3d248a5 Add flake.nix for development environment 2026-02-11 17:15:49 +01:00
Ded 2a3f2b3a24 Merge pull request #150 from ericszimmermann/german
translations to german updated.
2026-02-11 08:13:28 -08:00
Leah 675083fa01 Update .gitignore to exclude .gradle/ 2026-02-11 17:10:49 +01:00
Ded 5fc4b80b16 Merge pull request #144 from zjs81/support-whisperos
add support for whipseros
2026-02-11 08:03:44 -08:00
446564 84a32c1e67 remove wakelock
was being used to keep ble active which is not what it does

in early testing the ble remains connected with display off and also
when switching apps
2026-02-10 19:38:46 -08:00
ericz 607583060a translations to german updated. 2026-02-10 22:55:39 +01:00
Ded 71cf556b61 Merge pull request #148 from spfmoby/more-better-french-translations
Still better french translations
2026-02-10 11:24:47 -08:00
Zach c26174ad18 Chore bump version 2026-02-10 09:01:56 -07:00
spfmoby 04021a39a1 Better french translations 2026-02-10 08:12:51 +01:00
446564 fe23e9f7a0 add support for whipseros
needed a new ble prefix
2026-02-09 05:36:25 -08:00
Ded d7ec1876af Merge pull request #143 from zjs81/alpha6
chore: update version to alpha 6
2026-02-08 19:07:29 -08:00
91 changed files with 16003 additions and 4682 deletions
+2
View File
@@ -30,6 +30,7 @@ migrate_working_dir/
.flutter-plugins-dependencies
.pub-cache/
.pub/
pubspec.lock
/build/
/coverage/
@@ -65,6 +66,7 @@ secrets.dart
**/ios/Flutter/Flutter.podspec
# Android
.gradle/
**/android/.gradle/
**/android/captures/
**/android/local.properties
+1
View File
@@ -0,0 +1 @@
4.0.0
+1
View File
@@ -78,6 +78,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
-**Android**: Full support (API 21+)
-**iOS**: Full support (iOS 12+)
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
- 🚧 **Web**: Under construction (Chrome)
### Dependencies
+4 -4
View File
@@ -19,13 +19,13 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
@@ -83,5 +83,5 @@ flutter {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M268-240 42-466l57-56 170 170 56 56-57 56Zm226 0L268-466l56-57 170 170 368-368 56 57-424 424Zm0-226-57-56 198-198 57 56-198 198Z"/></svg>

After

Width:  |  Height:  |  Size: 253 B

Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770562336,
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+86
View File
@@ -0,0 +1,86 @@
{
description = "MeshCore Flutter Application";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Flutter and Dart
flutter
dart
# Java (required for Android development)
jdk17
# Android development tools
android-tools
gradle
# For the shell hook to set up the environment for Flutter development
gtk3
glib
sysprof
libclang
cmake
ninja
pkg-config
libdatrie
# Additional tools for installing Android SDK if not present
curl
unzip
];
shellHook = ''
echo "MeshCore Flutter Development Environment"
export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH"
export CMAKE_INSTALL_PREFIX="$PWD/build/bundle"
# Setup Android SDK in home directory (standard location)
export ANDROID_HOME="$HOME/Android/Sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH"
echo "Android SDK: $ANDROID_HOME"
echo ""
# Check if Android SDK exists and offer to download if not
if [ ! -d "$ANDROID_HOME" ]; then
echo "WARNING: Android SDK not found at $ANDROID_HOME"
echo ""
echo "To download and set up the Android SDK, run this command:"
echo ""
cat << 'EOF'
mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \
curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \
unzip -q cmdline-tools.zip && \
mkdir -p cmdline-tools/latest && \
mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \
rm cmdline-tools.zip && \
cd cmdline-tools/latest/bin && \
yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \
echo "Android SDK setup complete!"
EOF
echo ""
echo "Then run 'flutter doctor' again to verify."
echo ""
else
echo "Android SDK found at $ANDROID_HOME"
fi
echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues."
'';
};
}
);
}
+602 -41
View File
@@ -5,7 +5,6 @@ import 'package:crypto/crypto.dart' as crypto;
import 'package:pointycastle/export.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import '../models/channel.dart';
import '../models/channel_message.dart';
@@ -30,6 +29,7 @@ import '../storage/contact_store.dart';
import '../storage/message_store.dart';
import '../storage/unread_store.dart';
import '../utils/app_logger.dart';
import '../utils/battery_utils.dart';
import 'meshcore_protocol.dart';
class MeshCoreUuids {
@@ -38,6 +38,42 @@ class MeshCoreUuids {
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
}
class DirectRepeater {
static const int maxAgeMinutes = 30; // Max age for direct repeater info
final int pubkeyFirstByte;
double snr;
DateTime lastUpdated;
DirectRepeater({
required this.pubkeyFirstByte,
required this.snr,
DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now();
void update(double newSNR) {
snr = newSNR;
lastUpdated = DateTime.now();
}
int get ranking {
if (isStale()) {
return -1; // Stale repeaters get lowest rank
}
// Higher SNR gets higher rank and recency within maxAgeMinutes breaks ties.
final ageMs =
DateTime.now().millisecondsSinceEpoch -
lastUpdated.millisecondsSinceEpoch;
final maxAgeMs = maxAgeMinutes * 60 * 1000;
final recencyScore = (maxAgeMs - ageMs).clamp(0, maxAgeMs);
return ((snr - 31.75) * 1000).round() + recencyScore;
}
bool isStale() {
return DateTime.now().difference(lastUpdated) >
const Duration(minutes: maxAgeMinutes);
}
}
enum MeshCoreConnectionState {
disconnected,
scanning,
@@ -46,6 +82,18 @@ enum MeshCoreConnectionState {
disconnecting,
}
class RepeaterBatterySnapshot {
final int millivolts;
final DateTime updatedAt;
final String source;
const RepeaterBatterySnapshot({
required this.millivolts,
required this.updatedAt,
required this.source,
});
}
class MeshCoreConnector extends ChangeNotifier {
// Message windowing to limit memory usage
static const int _messageWindowSize = 200;
@@ -66,6 +114,10 @@ class MeshCoreConnector extends ChangeNotifier {
final List<Channel> _channels = [];
final Map<String, List<Message>> _conversations = {};
final Map<int, List<ChannelMessage>> _channelMessages = {};
final List<String> _pendingChannelSentQueue = [];
final List<_PendingCommandAck> _pendingGenericAckQueue = [];
static const String _reactionSendQueuePrefix = '__reaction_send__';
int _reactionSendQueueSequence = 0;
final Set<String> _loadedConversationKeys = {};
final Map<int, Set<String>> _processedChannelReactions =
{}; // channelIndex -> Set of "targetHash_emoji"
@@ -91,9 +143,12 @@ class MeshCoreConnector extends ChangeNotifier {
int? _currentBwHz;
int? _currentSf;
int? _currentCr;
bool? _clientRepeat;
int? _firmwareVerCode;
int? _batteryMillivolts;
double? _selfLatitude;
double? _selfLongitude;
final List<DirectRepeater> _directRepeaters = List.empty(growable: true);
bool _isLoadingContacts = false;
bool _isLoadingChannels = false;
bool _hasLoadedChannels = false;
@@ -149,6 +204,7 @@ class MeshCoreConnector extends ChangeNotifier {
final Map<String, bool> _contactSmazEnabled = {};
final Set<String> _knownContactKeys = {};
final Map<String, int> _contactUnreadCount = {};
final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
bool _unreadStateLoaded = false;
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
String? _activeContactKey;
@@ -195,12 +251,15 @@ class MeshCoreConnector extends ChangeNotifier {
String? get selfName => _selfName;
double? get selfLatitude => _selfLatitude;
double? get selfLongitude => _selfLongitude;
List<DirectRepeater> get directRepeaters => _directRepeaters;
int? get currentTxPower => _currentTxPower;
int? get maxTxPower => _maxTxPower;
int? get currentFreqHz => _currentFreqHz;
int? get currentBwHz => _currentBwHz;
int? get currentSf => _currentSf;
int? get currentCr => _currentCr;
bool? get clientRepeat => _clientRepeat;
int? get firmwareVerCode => _firmwareVerCode;
Map<String, String>? get currentCustomVars => _currentCustomVars;
int? get batteryMillivolts => _batteryMillivolts;
int get maxContacts => _maxContacts;
@@ -213,10 +272,32 @@ class MeshCoreConnector extends ChangeNotifier {
: 0;
int? get batteryPercent => _batteryMillivolts == null
? null
: _estimateBatteryPercent(
: estimateBatteryPercentFromMillivolts(
_batteryMillivolts!,
_batteryChemistryForDevice(),
);
RepeaterBatterySnapshot? getRepeaterBatterySnapshot(String contactKeyHex) =>
_repeaterBatterySnapshots[contactKeyHex];
int? getRepeaterBatteryMillivolts(String contactKeyHex) =>
_repeaterBatterySnapshots[contactKeyHex]?.millivolts;
void updateRepeaterBatterySnapshot(
String contactKeyHex,
int millivolts, {
String source = 'unknown',
}) {
if (contactKeyHex.isEmpty || millivolts <= 0) return;
final previous = _repeaterBatterySnapshots[contactKeyHex];
final snapshot = RepeaterBatterySnapshot(
millivolts: millivolts,
updatedAt: DateTime.now(),
source: source,
);
_repeaterBatterySnapshots[contactKeyHex] = snapshot;
if (previous?.millivolts != millivolts) {
notifyListeners();
}
}
String _batteryChemistryForDevice() {
final deviceId = _device?.remoteId.toString();
@@ -224,27 +305,6 @@ class MeshCoreConnector extends ChangeNotifier {
return _appSettingsService!.batteryChemistryForDevice(deviceId);
}
int _estimateBatteryPercent(int millivolts, String chemistry) {
final range = _batteryVoltageRange(chemistry);
final minMv = range.$1;
final maxMv = range.$2;
if (millivolts <= minMv) return 0;
if (millivolts >= maxMv) return 100;
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
}
(int, int) _batteryVoltageRange(String chemistry) {
switch (chemistry) {
case 'lifepo4':
return (2600, 3650);
case 'lipo':
return (3000, 4200);
case 'nmc':
default:
return (3000, 4200);
}
}
List<Message> getMessages(Contact contact) {
return _conversations[contact.publicKeyHex] ?? [];
}
@@ -609,6 +669,7 @@ class MeshCoreConnector extends ChangeNotifier {
publicKey: contact.publicKey,
name: contact.name,
type: contact.type,
flags: contact.flags,
pathLength: selection.hopCount >= 0
? selection.hopCount
: contact.pathLength,
@@ -658,7 +719,8 @@ class MeshCoreConnector extends ChangeNotifier {
_scanResults.clear();
for (var result in results) {
if (result.device.platformName.startsWith("MeshCore-") ||
result.advertisementData.advName.startsWith("MeshCore-")) {
result.advertisementData.advName.startsWith("MeshCore-") ||
result.advertisementData.advName.startsWith("Whisper-")) {
_scanResults.add(result);
}
}
@@ -775,9 +837,6 @@ class MeshCoreConnector extends ChangeNotifier {
_setState(MeshCoreConnectionState.connected);
// Enable wake lock to prevent BLE disconnection when screen turns off
await WakelockPlus.enable();
await _requestDeviceInfo();
_startBatteryPolling();
final gotSelfInfo = await _waitForSelfInfo(
@@ -886,9 +945,6 @@ class MeshCoreConnector extends ChangeNotifier {
_setState(MeshCoreConnectionState.disconnecting);
_stopBatteryPolling();
// Disable wake lock when disconnecting
await WakelockPlus.disable();
await _notifySubscription?.cancel();
_notifySubscription = null;
@@ -922,7 +978,10 @@ class MeshCoreConnector extends ChangeNotifier {
_selfName = null;
_selfLatitude = null;
_selfLongitude = null;
_clientRepeat = null;
_firmwareVerCode = null;
_batteryMillivolts = null;
_repeaterBatterySnapshots.clear();
_batteryRequested = false;
_awaitingSelfInfo = false;
_maxContacts = _defaultMaxContacts;
@@ -934,6 +993,9 @@ class MeshCoreConnector extends ChangeNotifier {
_isSyncingChannels = false;
_channelSyncInFlight = false;
_hasLoadedChannels = false;
_pendingChannelSentQueue.clear();
_pendingGenericAckQueue.clear();
_reactionSendQueueSequence = 0;
_setState(MeshCoreConnectionState.disconnected);
if (!manual) {
@@ -941,7 +1003,11 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
Future<void> sendFrame(Uint8List data) async {
Future<void> sendFrame(
Uint8List data, {
String? channelSendQueueId,
bool expectsGenericAck = false,
}) async {
if (!isConnected || _rxCharacteristic == null) {
throw Exception("Not connected to a MeshCore device");
}
@@ -960,6 +1026,11 @@ class MeshCoreConnector extends ChangeNotifier {
data.toList(),
withoutResponse: canWriteWithoutResponse,
);
_trackPendingGenericAck(
data,
channelSendQueueId: channelSendQueueId,
expectsGenericAck: expectsGenericAck,
);
}
Future<void> requestBatteryStatus({bool force = false}) async {
@@ -1115,11 +1186,78 @@ class MeshCoreConnector extends ChangeNotifier {
customPath,
pathLen,
type: contact.type,
flags: contact.flags,
name: contact.name,
),
);
}
Future<void> setContactFavorite(Contact contact, bool isFavorite) async {
if (!isConnected) return;
final latestContact =
await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact;
final updatedFlags = isFavorite
? (latestContact.flags | contactFlagFavorite)
: (latestContact.flags & ~contactFlagFavorite);
await sendFrame(
buildUpdateContactPathFrame(
latestContact.publicKey,
latestContact.path,
latestContact.pathLength,
type: latestContact.type,
flags: updatedFlags,
name: latestContact.name,
),
);
final index = _contacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
if (index >= 0) {
_contacts[index] = _contacts[index].copyWith(
type: latestContact.type,
name: latestContact.name,
pathLength: latestContact.pathLength,
path: latestContact.path,
flags: updatedFlags,
);
notifyListeners();
unawaited(_persistContacts());
}
}
Future<Contact?> _fetchContactSnapshotFromDevice(
Uint8List pubKey, {
Duration timeout = const Duration(seconds: 3),
}) async {
if (!isConnected) return null;
final expectedKeyHex = pubKeyToHex(pubKey);
final completer = Completer<Contact?>();
void finish(Contact? result) {
if (!completer.isCompleted) {
completer.complete(result);
}
}
final subscription = receivedFrames.listen((frame) {
if (frame.isEmpty || frame[0] != respCodeContact) return;
final parsed = Contact.fromFrame(frame);
if (parsed == null || parsed.publicKeyHex != expectedKeyHex) return;
finish(parsed);
});
final timer = Timer(timeout, () => finish(null));
try {
await getContactByKey(pubKey);
return await completer.future;
} finally {
timer.cancel();
await subscription.cancel();
}
}
/// Set path override for a contact (persists across contact refreshes)
/// pathLen: -1 = force flood, null = auto (use device path), >= 0 = specific path
Future<void> setPathOverride(
@@ -1315,7 +1453,13 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
// Send the reaction to the device (don't add as a visible message)
await sendFrame(buildSendChannelTextMsgFrame(channel.index, text));
final reactionQueueId = _nextReactionSendQueueId();
_pendingChannelSentQueue.add(reactionQueueId);
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, text),
channelSendQueueId: reactionQueueId,
expectsGenericAck: true,
);
return;
}
@@ -1325,6 +1469,7 @@ class MeshCoreConnector extends ChangeNotifier {
channel.index,
);
_addChannelMessage(channel.index, message);
_pendingChannelSentQueue.add(message.messageId);
notifyListeners();
final trimmed = text.trim();
@@ -1334,7 +1479,11 @@ class MeshCoreConnector extends ChangeNotifier {
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
? Smaz.encodeIfSmaller(text)
: text;
await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText));
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, outboundText),
channelSendQueueId: message.messageId,
expectsGenericAck: true,
);
}
Future<void> removeContact(Contact contact) async {
@@ -1681,6 +1830,9 @@ class MeshCoreConnector extends ChangeNotifier {
debugPrint('RX frame: code=$code len=${frame.length}');
switch (code) {
case respCodeOk:
_handleOk();
break;
case respCodeDeviceInfo:
_handleDeviceInfo(frame);
break;
@@ -1696,6 +1848,11 @@ class MeshCoreConnector extends ChangeNotifier {
_isLoadingContacts = true;
notifyListeners();
break;
case pushCodeNewAdvert:
debugPrint('Got New CONTACT');
// It's the same format as respCodeContact, so we can reuse the handler
_handleContact(frame);
break;
case respCodeContact:
debugPrint('Got CONTACT');
_handleContact(frame);
@@ -1740,6 +1897,7 @@ class MeshCoreConnector extends ChangeNotifier {
case pushCodeStatusResponse:
break;
case pushCodeLogRxData:
_handleRxData(frame);
_handleLogRxData(frame);
break;
case respCodeChannelInfo:
@@ -1753,11 +1911,35 @@ class MeshCoreConnector extends ChangeNotifier {
break;
case respCodeCustomVars:
_handleCustomVars(frame);
break;
// RESP_CODE_ERR is a defined firmware response (code 1), not an unknown frame.
case respCodeErr:
_handleErrorFrame(frame);
break;
default:
debugPrint('Unknown frame code: $code');
}
}
void _handleErrorFrame(Uint8List frame) {
final errCode = frame.length > 1 ? frame[1] : -1;
_appDebugLogService?.warn(
'Firmware responded with error code: $errCode',
tag: 'Protocol',
);
if (_pendingGenericAckQueue.isEmpty) {
return;
}
final failedAck = _pendingGenericAckQueue.removeAt(0);
if (failedAck.commandCode != cmdSendChannelTxtMsg ||
failedAck.channelSendQueueId == null) {
return;
}
_pendingChannelSentQueue.remove(failedAck.channelSendQueueId);
}
void _handlePathUpdated(Uint8List frame) {
// Frame format: [0]=code, [1-32]=pub_key
if (frame.length >= 33 && _pathHistoryService != null) {
@@ -1826,6 +2008,13 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleDeviceInfo(Uint8List frame) {
if (frame.length < 4) return;
_firmwareVerCode = frame[1];
// Parse client_repeat from firmware v9+ (byte 80)
if (frame.length >= 81) {
_clientRepeat = frame[80] != 0;
}
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
final reportedContacts = frame[2];
final reportedChannels = frame[3];
@@ -1846,8 +2035,8 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(getChannels(maxChannels: nextMaxChannels));
}
}
notifyListeners();
}
notifyListeners();
}
void _handleNoMoreMessages() {
@@ -2008,6 +2197,80 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
void _handleContactAdvert(Contact contact) {
if (listEquals(contact.publicKey, _selfPublicKey)) {
return;
}
if (contact.type == advTypeRepeater) {
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
);
}
// Check if this is a new contact
final isNewContact = !_knownContactKeys.contains(contact.publicKeyHex);
final existingIndex = _contacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
if (existingIndex >= 0) {
final existing = _contacts[existingIndex];
final mergedLastMessageAt =
existing.lastMessageAt.isAfter(contact.lastMessageAt)
? existing.lastMessageAt
: contact.lastMessageAt;
appLogger.info(
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
tag: 'Connector',
);
// CRITICAL: Preserve user's path override when contact is refreshed from device
_contacts[existingIndex] = contact.copyWith(
lastMessageAt: mergedLastMessageAt,
pathOverride: existing.pathOverride, // Preserve user's path choice
pathOverrideBytes: existing.pathOverrideBytes,
);
appLogger.info(
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
tag: 'Connector',
);
} else {
_contacts.add(contact);
appLogger.info(
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
tag: 'Connector',
);
}
_knownContactKeys.add(contact.publicKeyHex);
_loadMessagesForContact(contact.publicKeyHex);
// Add path to history if we have a valid path
if (_pathHistoryService != null && contact.pathLength >= 0) {
_pathHistoryService!.handlePathUpdated(contact);
}
notifyListeners();
// Show notification for new contact (advertisement)
if (isNewContact && _appSettingsService != null) {
final settings = _appSettingsService!.settings;
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
_notificationService.showAdvertNotification(
contactName: contact.name,
contactType: contact.typeLabel,
contactId: contact.publicKeyHex,
);
}
}
if (!_isLoadingContacts) {
unawaited(_persistContacts());
}
}
Future<void> _persistContacts() async {
await _contactStore.saveContacts(_contacts);
}
@@ -2334,6 +2597,8 @@ class MeshCoreConnector extends ChangeNotifier {
}
final label = channelName ?? _channelDisplayName(channelIndex);
if (_appSettingsService!.isChannelMuted(label)) return;
_notificationService.showChannelMessageNotification(
channelName: label,
message: message.text,
@@ -2457,8 +2722,22 @@ class MeshCoreConnector extends ChangeNotifier {
return;
}
if (_retryService != null) {
_retryService!.updateMessageFromSent(ackHash, timeoutMs);
final retryService = _retryService;
if (retryService != null &&
retryService.updateMessageFromSent(
ackHash,
timeoutMs,
allowQueueFallback: false,
)) {
return;
}
if (_markNextPendingChannelMessageSent()) {
return;
}
if (retryService != null) {
retryService.updateMessageFromSent(ackHash, timeoutMs);
}
} else {
// Fallback to old behavior
@@ -2475,6 +2754,64 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
bool _markNextPendingChannelMessageSent() {
while (_pendingChannelSentQueue.isNotEmpty) {
final queuedMessageId = _pendingChannelSentQueue.removeAt(0);
if (_isReactionSendQueueId(queuedMessageId)) {
return true;
}
if (_markPendingChannelMessageSentById(queuedMessageId)) {
return true;
}
}
return false;
}
bool _markPendingChannelMessageSentById(String messageId) {
for (final entry in _channelMessages.entries) {
final channelMessages = entry.value;
for (int i = channelMessages.length - 1; i >= 0; i--) {
final message = channelMessages[i];
if (message.messageId != messageId) {
continue;
}
if (!message.isOutgoing ||
message.status != ChannelMessageStatus.pending) {
return false;
}
channelMessages[i] = message.copyWith(
status: ChannelMessageStatus.sent,
);
_pendingChannelSentQueue.remove(messageId);
unawaited(
_channelMessageStore.saveChannelMessages(entry.key, channelMessages),
);
notifyListeners();
return true;
}
}
return false;
}
void _handleOk() {
if (_pendingGenericAckQueue.isEmpty) {
return;
}
final pendingAck = _pendingGenericAckQueue.removeAt(0);
if (pendingAck.commandCode != cmdSendChannelTxtMsg ||
pendingAck.channelSendQueueId == null) {
return;
}
final queueId = pendingAck.channelSendQueueId!;
_pendingChannelSentQueue.remove(queueId);
if (_isReactionSendQueueId(queueId)) {
return;
}
_markPendingChannelMessageSentById(queueId);
}
void _handleSendConfirmed(Uint8List frame) {
// Frame format from C++:
// [0] = PUSH_CODE_SEND_CONFIRMED
@@ -3053,18 +3390,22 @@ class MeshCoreConnector extends ChangeNotifier {
mergedPathBytes.length,
);
final newRepeatCount = existing.repeatCount + 1;
final promotedFromPending =
newRepeatCount == 1 &&
existing.status == ChannelMessageStatus.pending;
messages[existingIndex] = existing.copyWith(
repeatCount: newRepeatCount,
pathLength: mergedPathLength,
pathBytes: mergedPathBytes,
pathVariants: mergedPathVariants,
// Mark as sent when first repeat is heard
status:
newRepeatCount == 1 &&
existing.status == ChannelMessageStatus.pending
status: promotedFromPending
? ChannelMessageStatus.sent
: existing.status,
);
if (promotedFromPending) {
_pendingChannelSentQueue.remove(existing.messageId);
}
} else {
messages.add(processedMessage);
}
@@ -3214,8 +3555,6 @@ class MeshCoreConnector extends ChangeNotifier {
}
void _handleDisconnection() {
// Disable wake lock when connection is lost
WakelockPlus.disable();
_stopBatteryPolling();
for (final entry in _pendingRepeaterAcks.values) {
@@ -3239,11 +3578,37 @@ class MeshCoreConnector extends ChangeNotifier {
_queuedMessageSyncInFlight = false;
_isSyncingChannels = false;
_channelSyncInFlight = false;
_pendingChannelSentQueue.clear();
_pendingGenericAckQueue.clear();
_reactionSendQueueSequence = 0;
_setState(MeshCoreConnectionState.disconnected);
_scheduleReconnect();
}
void _trackPendingGenericAck(
Uint8List data, {
String? channelSendQueueId,
required bool expectsGenericAck,
}) {
if (!expectsGenericAck || data.isEmpty) return;
_pendingGenericAckQueue.add(
_PendingCommandAck(
commandCode: data[0],
channelSendQueueId: channelSendQueueId,
),
);
}
String _nextReactionSendQueueId() {
_reactionSendQueueSequence++;
return '$_reactionSendQueuePrefix$_reactionSendQueueSequence';
}
bool _isReactionSendQueueId(String queueId) {
return queueId.startsWith(_reactionSendQueuePrefix);
}
Map<String, String> _parseKeyValueString(String input) {
final result = <String, String>{};
@@ -3269,7 +3634,11 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleCustomVars(Uint8List frame) {
final buf = BufferReader(frame.sublist(1));
try {
_currentCustomVars = _parseKeyValueString(buf.readString());
} catch (e) {
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
}
}
void _setState(MeshCoreConnectionState newState) {
@@ -3293,6 +3662,191 @@ class MeshCoreConnector extends ChangeNotifier {
super.dispose();
}
void _handleRxData(Uint8List frame) {
final packet = BufferReader(frame);
double snr = 0.0;
int routeType = 0;
int payloadType = 0;
Uint8List pathBytes = Uint8List(0);
Uint8List payload = Uint8List(0);
try {
packet.skipBytes(1); // Skip frame type byte
snr = packet.readInt8() / 4.0;
packet.skipBytes(1); // Skip RSSI byte
//final rssi = packet.readByte();
final header = packet.readByte();
routeType = header & 0x03;
payloadType = (header >> 2) & 0x0F;
//final payloadVer = (header >> 6) & 0x03;
final pathLen = packet.readByte();
pathBytes = packet.readBytes(pathLen);
payload = packet.readBytes(packet.remaining);
} catch (e) {
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
return;
}
switch (payloadType) {
case payloadTypeADVERT:
_handlePayloadAdvertReceived(payload, pathBytes, routeType, snr);
break;
default:
}
}
void _handlePayloadAdvertReceived(
Uint8List frame,
Uint8List path,
int routeType,
double snr,
) {
final advert = BufferReader(frame);
double latitude = 0.0;
double longitude = 0.0;
String name = '';
String contactKeyHex = '';
Uint8List publicKey = Uint8List(0);
int type = 0;
int timestamp = 0;
bool hasLocation = false;
bool hasName = false;
try {
publicKey = advert.readBytes(32);
contactKeyHex = publicKey
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
timestamp = advert.readInt32LE();
//TODO add signature verification
advert.skipBytes(64); // Skip signature for now
final flags = advert.readByte();
type = flags & 0x0F;
hasLocation = (flags & 0x10) != 0;
// For future use:
//final hasFeature1 = (flags & 0x20) != 0;
//final hasFeature2 = (flags & 0x40) != 0;
hasName = (flags & 0x80) != 0;
if (hasLocation && advert.remaining >= 8) {
latitude = advert.readInt32LE() / 1e6;
longitude = advert.readInt32LE() / 1e6;
}
if (hasName && advert.remaining > 0) {
name = advert.readString();
}
} catch (e) {
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
return;
}
if (listEquals(publicKey, _selfPublicKey)) {
return;
}
// Check if this is a new contact
final isNewContact = !_knownContactKeys.contains(contactKeyHex);
if (isNewContact) {
final newContact = Contact(
publicKey: publicKey,
name: name,
type: type,
pathLength: path.length,
path: Uint8List.fromList(
path.reversed.toList(),
), // Store path in reverse for easier use in outgoing messages
latitude: latitude,
longitude: longitude,
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
);
_handleContactAdvert(newContact);
_updateDirectRepeater(newContact, snr, path);
return;
}
final existingIndex = _contacts.indexWhere(
(c) => c.publicKeyHex == contactKeyHex,
);
if (existingIndex >= 0) {
final existing = _contacts[existingIndex];
final mergedLastMessageAt = existing.lastMessageAt.isAfter(DateTime.now())
? DateTime.now()
: existing.lastMessageAt;
appLogger.info(
'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}',
tag: 'Connector',
);
// CRITICAL: Preserve user's path override when contact is refreshed from device
_contacts[existingIndex] = existing.copyWith(
latitude: hasLocation ? latitude : existing.latitude,
longitude: hasLocation ? longitude : existing.longitude,
name: hasName ? name : existing.name,
path: Uint8List.fromList(path.reversed.toList()),
pathLength: path.length,
lastMessageAt: mergedLastMessageAt,
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
pathOverride: existing.pathOverride, // Preserve user's path choice
pathOverrideBytes: existing.pathOverrideBytes,
);
// Add path to history if we have a valid path
if (_pathHistoryService != null &&
_contacts[existingIndex].pathLength >= 0) {
_pathHistoryService!.handlePathUpdated(_contacts[existingIndex]);
}
_updateDirectRepeater(_contacts[existingIndex], snr, path);
appLogger.info(
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
tag: 'Connector',
);
}
}
void _updateDirectRepeater(Contact contact, double snr, Uint8List path) {
final pubkeyFirstByte = path.isNotEmpty
? path.last
: contact.publicKey.first;
_directRepeaters.removeWhere((r) => r.isStale());
//We can use adverts from chat and sensor nodes, but only if the advert has a path to get the last hop.
if ((contact.type == advTypeChat || contact.type == advTypeSensor) &&
path.isEmpty) {
notifyListeners();
return;
}
final isTracked = _directRepeaters.where(
(r) => r.pubkeyFirstByte == pubkeyFirstByte,
);
final sortedRepeaters = List<DirectRepeater>.from(_directRepeaters)
..sort((a, b) => b.snr.compareTo(a.snr));
final weakestRepeater = sortedRepeaters.isNotEmpty
? sortedRepeaters.last
: null;
if (_directRepeaters.length >= 5 &&
weakestRepeater != null &&
isTracked.isEmpty) {
_directRepeaters.remove(weakestRepeater);
}
if (isTracked.isNotEmpty) {
final repeater = isTracked.first;
repeater.update(snr);
} else if (_directRepeaters.length < 5) {
_directRepeaters.add(
DirectRepeater(pubkeyFirstByte: pubkeyFirstByte, snr: snr),
);
}
notifyListeners();
}
}
const int _phRouteMask = 0x03;
@@ -3350,3 +3904,10 @@ class _RepeaterAckContext {
required this.messageBytes,
});
}
class _PendingCommandAck {
final int commandCode;
final String? channelSendQueueId;
_PendingCommandAck({required this.commandCode, this.channelSendQueueId});
}
+69 -3
View File
@@ -13,12 +13,22 @@ class BufferReader {
int readByte() => readBytes(1)[0];
Uint8List readBytes(int count) {
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}',
);
}
final data = _buffer.sublist(_pointer, _pointer + count);
_pointer += count;
return data;
}
void skipBytes(int count) {
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}',
);
}
_pointer += count;
}
@@ -151,6 +161,7 @@ const int cmdGetContactByKey = 30;
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 cmdGetCustomVar = 40;
@@ -166,7 +177,7 @@ const int reqTypeGetStatus = 0x01;
const int reqTypeKeepAlive = 0x02;
const int reqTypeGetTelemetry = 0x03;
const int reqTypeGetAccessList = 0x05;
const int reqTypeGetNeighbours = 0x06;
const int reqTypeGetNeighbors = 0x06;
// Repeater response codes
const int respServerLoginOk = 0;
@@ -212,6 +223,30 @@ const int advTypeRepeater = 2;
const int advTypeRoom = 3;
const int advTypeSensor = 4;
// Payload Types
const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
const int payloadTypeRESPONSE =
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
const int payloadTypeTXTMSG =
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
const int payloadTypeACK = 0x03; // a simple ack
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
const int payloadTypeGRPTXT =
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
const int payloadTypeGRPDATA =
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
const int payloadTypeANONREQ =
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
const int payloadTypePATH =
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
//...
const int payloadTypeRawCustom =
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
// Sizes
const int pubKeySize = 32;
const int maxPathSize = 64;
@@ -255,6 +290,7 @@ int _minPositive(int a, int b) {
const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33;
const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01;
const int contactPathLenOffset = 35;
const int contactPathOffset = 36;
const int contactNameOffset = 100;
@@ -550,18 +586,29 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) {
}
// Build CMD_SET_RADIO_PARAMS frame
// Format: [cmd][freq x4][bw x4][sf][cr]
// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9)
// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+)
// freq: frequency in Hz (300000-2500000)
// bw: bandwidth in Hz (7000-500000)
// sf: spreading factor (5-12)
// cr: coding rate (5-8)
Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) {
// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older)
Uint8List buildSetRadioParamsFrame(
int freqHz,
int bwHz,
int sf,
int cr, {
bool? clientRepeat,
}) {
final writer = BufferWriter();
writer.writeByte(cmdSetRadioParams);
writer.writeUInt32LE(freqHz);
writer.writeUInt32LE(bwHz);
writer.writeByte(sf);
writer.writeByte(cr);
if (clientRepeat != null) {
writer.writeByte(clientRepeat ? 1 : 0);
}
return writer.toBytes();
}
@@ -777,3 +824,22 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
writer.writeBytes(pubKey);
return writer.toBytes();
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowAutoAddContacts][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
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
writer.writeByte(multiAcks); // Multi Acknowledgements
return writer.toBytes();
}
+19 -5
View File
@@ -1,4 +1,6 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class CayenneLpp {
@@ -84,7 +86,7 @@ class CayenneLpp {
static List<Map<String, dynamic>> parse(Uint8List bytes) {
final buffer = BufferReader(bytes);
final telemetry = <Map<String, dynamic>>[];
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
@@ -186,14 +188,19 @@ class CayenneLpp {
return telemetry;
}
}
return telemetry;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
// Return any telemetry parsed so far to preserve partial data
return telemetry;
}
}
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
final buffer = BufferReader(bytes);
final Map<int, Map<String, dynamic>> channels = {};
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
@@ -251,13 +258,20 @@ class CayenneLpp {
break;
// Add more types as needed...
default:
// Unknown type: skip or handle error?
continue;
//Stopped parsing to avoid misalignment
return channels.values.toList();
}
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
return <
Map<String, dynamic>
>[]; // Return an empty list on error to avoid crashing the app
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class LosIcon extends StatelessWidget {
final double size;
final Color? color;
const LosIcon({super.key, this.size = 24, this.color});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconTheme = IconTheme.of(context);
final iconColor =
color ??
iconTheme.color ??
theme.iconTheme.color ??
theme.colorScheme.onSurface;
return Icon(Symbols.elevation, size: size, color: iconColor);
}
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "bg",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакти",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Брой контакти",
"settings_infoChannelCount": "Брой канали",
"settings_presets": "Предварителни настройки",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Честота (MHz)",
"settings_frequencyHelper": "300.0 - 2500.0",
"settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Мощност (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)",
"settings_longRange": "Дълъг обхват",
"settings_fastSpeed": "Бърза скорост",
"settings_error": "Грешка: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Публичен канал",
"channels_privateChannel": "Частен канал",
"channels_editChannel": "Редактирай канал",
"channels_muteChannel": "Заглуши канала",
"channels_unmuteChannel": "Включи известията на канала",
"channels_deleteChannel": "Изтрий канала",
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbours": "Съседи",
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbors": "Съседи",
"neighbors_receivedData": "Получени данни за съседи",
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
"neighbors_repeatersNeighbours": "Повторители Съседи",
"neighbors_repeatersNeighbors": "Повторители Съседи",
"neighbors_noData": "Няма налични данни за съседи.",
"channels_createPrivateChannel": "Създай Частен Канал",
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
@@ -1557,6 +1562,8 @@
"contacts_clipboardEmpty": "Клипборда е празна.",
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
"appSettings_languageRu": "Руски",
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
"contacts_contactImported": "Контактът е импортиран.",
"contacts_zeroHopAdvert": "Реклама без скок",
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}",
"notification_newTypeDiscovered": "Открит нов {contactType}",
"notification_receivedNewMessage": "Получено ново съобщение",
"contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.",
"settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.",
"settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.",
"settings_gpxExportAll": "Експортирай всички контакти в GPX",
@@ -1591,6 +1597,207 @@
"settings_gpxExportAllContacts": "Местоположения на всички контакти",
"settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX",
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!"
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!",
"map_pathTraceCancelled": "Отменен е следването на пътя.",
"pathTrace_clearTooltip": "Изчисти пътя",
"map_removeLast": "Премахни Последно",
"map_runTrace": "Изпълни Път на Следване",
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
"scanner_bluetoothOff": "Bluetooth е изключен.",
"scanner_enableBluetooth": "Активирайте Bluetooth",
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
"snrIndicator_lastSeen": "Последно видян",
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
"chat_ShowAllPaths": "Покажи всички пътища",
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
"settings_clientRepeat": "Без електричество – повторение",
"settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "единици",
"appSettings_unitsMetric": "Метрика (m / km)",
"appSettings_unitsImperial": "Имперска (ft / mi)",
"map_lineOfSight": "Линия на видимост",
"map_losScreenTitle": "Линия на видимост",
"losSelectStartEnd": "Изберете начални и крайни възли за LOS.",
"losRunFailed": "Проверката на пряката видимост е неуспешна: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Изчистете всички точки",
"losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина",
"losMenuTitle": "LOS меню",
"losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки",
"losShowDisplayNodes": "Показване на възли на дисплея",
"losCustomPoints": "Персонализирани точки",
"losCustomPointLabel": "Персонализирано {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Точка А",
"losPointB": "Точка Б",
"losAntennaA": "Антена A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Антена B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Стартирайте LOS",
"losNoElevationData": "Няма данни за надморска височина",
"losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: проверка...",
"losStatusNoData": "LOS: няма данни",
"losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.",
"losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.",
"losRenameCustomPoint": "Преименувайте персонализирана точка",
"losPointName": "Име на точката",
"losShowPanelTooltip": "Показване на LOS панел",
"losHidePanelTooltip": "Скриване на LOS панела",
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Радиохоризонт",
"losLegendLosBeam": "Линия на видимост",
"losLegendTerrain": "Терен",
"losFrequencyLabel": "Честота",
"losFrequencyInfoTooltip": "Преглед на детайли за изчислението",
"losFrequencyDialogTitle": "Изчисляване на радиохоризонта",
"losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Премахване от списъка с любими",
"listFilter_addToFavorites": "Добави към любими",
"listFilter_favorites": "Любими",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchFavorites": "Търсене на {number}{str} любими...",
"contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...",
"contacts_unread": "Непрочетено",
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
"contacts_searchContactsNoNumber": "Търси контакти...",
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
}
+267 -53
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "de",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakte",
@@ -96,14 +104,14 @@
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
"settings_actions": "Aktionen",
"settings_sendAdvertisement": "Sende eine Ankündigung",
"settings_sendAdvertisementSubtitle": "Sende Ankündigung",
"settings_sendAdvertisement": "Sende Ankündigung",
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
"settings_advertisementSent": "Ankündigung gesendet",
"settings_syncTime": "Zeitsynchronisierung",
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
"settings_timeSynchronized": "Zeit synchronisiert",
"settings_refreshContacts": "Kontakte aktualisieren",
"settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden",
"settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden",
"settings_rebootDevice": "Gerät neu starten",
"settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten",
"settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Anzahl Kontakte",
"settings_infoChannelCount": "Anzahl Kanäle",
"settings_presets": "Voreinstellungen",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequenz (MHz)",
"settings_frequencyHelper": "300,00 - 2.500,00",
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX-Leistung (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
"settings_longRange": "Grosse Reichweite",
"settings_fastSpeed": "Schnelle Geschwindigkeit",
"settings_error": "Fehler: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Öffentlicher Kanal",
"channels_privateChannel": "Privater Kanal",
"channels_editChannel": "Kanal bearbeiten",
"channels_muteChannel": "Kanal stummschalten",
"channels_unmuteChannel": "Kanal Stummschaltung aufheben",
"channels_deleteChannel": "Lösche den Kanal",
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
"@channels_deleteChannelConfirm": {
@@ -540,7 +545,7 @@
"chat_routingMode": "Routenmodus",
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"chat_forceFloodMode": "Flut-Modus erzwingen",
"chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):",
"chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):",
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
"chat_hopSingular": "Sprung",
"chat_hopPlural": "Sprünge",
@@ -554,7 +559,7 @@
},
"chat_successes": "Erfolgreich",
"chat_removePath": "Pfad entfernen",
"chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_noPathHistoryYet": "Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_pathActions": "Pfadaktionen:",
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
@@ -717,7 +722,7 @@
"mapCache_cacheArea": "Zwischenspeicherbereich",
"mapCache_useCurrentView": "Aktuelle Ansicht verwenden",
"mapCache_zoomRange": "Zoom Bereich",
"mapCache_estimatedTiles": "Geschätzte Fliesen: {count}",
"mapCache_estimatedTiles": "Geschätzte Kacheln: {count}",
"@mapCache_estimatedTiles": {
"placeholders": {
"count": {
@@ -854,7 +859,7 @@
},
"path_enterCustomPath": "Gebe Pfad ein",
"path_currentPathLabel": "Aktueller Pfad",
"path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
"path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.",
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
@@ -887,7 +892,7 @@
"repeater_forceFloodMode": "Flut-Modus erzwingen",
"repeater_pathManagement": "Pfadverwaltung",
"repeater_refresh": "Aktualisieren",
"repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.",
"repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.",
"repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}",
"@repeater_errorLoadingStatus": {
"placeholders": {
@@ -957,7 +962,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Überflut: {flood}, Direkt: {direct}",
"repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -983,7 +988,7 @@
"repeater_adminPassword": "Admin-Passwort",
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
"repeater_guestPassword": "Gast-Passwort",
"repeater_guestPasswordHelper": "Schreibgeschützter Zugriffspasswort",
"repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort",
"repeater_radioSettings": "Funk Einstellungen",
"repeater_frequencyMhz": "Frequenz (MHz)",
"repeater_frequencyHelper": "300-2500 MHz",
@@ -1086,11 +1091,11 @@
}
},
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Fehlersuche Nächster Befehl",
"repeater_debugNextCommand": "Fehlersuche des nächsten Befehls",
"repeater_commandHelp": "Hilfe",
"repeater_clearHistory": "Löschen der Historie",
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
"repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle",
"repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle",
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
"repeater_previousCommand": "Vorhergehende Aktion",
"repeater_nextCommand": "Nächste Aktion",
@@ -1132,7 +1137,7 @@
"repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.",
"repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).",
"repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.",
"repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.",
@@ -1143,14 +1148,14 @@
"repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).",
"repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).",
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp none, rs232, espnow ab.",
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.",
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.",
"repeater_cliHelpRegion": "Listet alle definierten Regionen auf.",
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.",
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingeckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.",
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
"repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)",
@@ -1243,7 +1248,7 @@
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
"channelPath_repeaterHops": "Repeater-Sprünge",
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
"channelPath_messageDetails": "Nachrichtsdetails",
"channelPath_messageDetails": "Nachrichtendetails",
"channelPath_senderLabel": "Sender",
"channelPath_timeLabel": "Zeit",
"channelPath_repeatsLabel": "Wiederholungen",
@@ -1344,10 +1349,13 @@
"listFilter_az": "A-Z",
"listFilter_filters": "Filtere",
"listFilter_all": "Alle",
"listFilter_favorites": "Favoriten",
"listFilter_addToFavorites": "Zu Favoriten hinzufügen",
"listFilter_removeFromFavorites": "Aus Favoriten entfernen",
"listFilter_users": "Benutzer",
"listFilter_repeaters": "Repeater",
"listFilter_roomServers": "Raumserver",
"listFilter_unreadOnly": "Nur nicht gelesen",
"listFilter_unreadOnly": "Nicht gelesen",
"listFilter_newGroup": "Neue Gruppe",
"@neighbors_errorLoading": {
"placeholders": {
@@ -1356,13 +1364,13 @@
}
}
},
"repeater_neighbours": "Nachbarn",
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"neighbors_receivedData": "Empfangene Nachbarendaten",
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
"repeater_neighbors": "Nachbarn",
"repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"neighbors_receivedData": "Empfangene Nachbarsdaten",
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
"neighbors_repeatersNeighbours": "Nachbarn",
"neighbors_noData": "Keine Nachbardaten verfügbar.",
"neighbors_repeatersNeighbors": "Nachbarn",
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
@@ -1389,8 +1397,8 @@
}
}
},
"neighbors_heardAgo": "Hörte: {time} vor her.",
"neighbors_unknownContact": "Unbekannte {pubkey}",
"neighbors_heardAgo": "Gehört vor: {time}",
"neighbors_unknownContact": "Unbekannt {pubkey}",
"settings_locationGPSEnable": "GPS aktivieren",
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
@@ -1493,9 +1501,9 @@
"community_deleted": "Community \"{name}\" verlassen",
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
"community_selectCommunity": "Wählen Sie Community",
"community_selectCommunity": "Wählen Sie eine Community",
"community_regularHashtag": "Regulärer Hashtag",
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
"community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)",
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
"community_forCommunity": "Für {name}",
"community_communityHashtag": "Community Hashtag",
@@ -1557,61 +1565,267 @@
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
"appSettings_languageUk": "Ukrainisch",
"appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren",
"appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen",
"contacts_contactImported": "Kontakt wurde importiert.",
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
"contacts_zeroHopAdvert": "Zero-Hop-Anzeige",
"contacts_floodAdvert": "Überflutungsanzeige",
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
"contacts_floodAdvert": "Flut-Ankündigung",
"contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen",
"contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen",
"contacts_copyAdvertToClipboard": "Werbung in die Zwischenablage kopieren",
"contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren",
"contacts_ShareContact": "Kontakt in die Zwischenablage kopieren",
"contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.",
"contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet",
"contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.",
"contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.",
"contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.",
"notification_activityTitle": "MeshCore Aktivität",
"notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}",
"@notification_messagesCount": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}",
"@notification_channelMessagesCount": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}",
"@notification_newNodesCount": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"notification_newTypeDiscovered": "Neuer {contactType} entdeckt",
"@notification_newTypeDiscovered": {
"placeholders": {
"contactType": {"type": "String"}
"contactType": {
"type": "String"
}
}
},
"notification_receivedNewMessage": "Neue Nachricht empfangen",
"contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.",
"settings_gpxExportAll": "Alle Kontakte nach GPX exportieren",
"settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.",
"settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren",
"settings_gpxExportContacts": "Begleiter nach GPX exportieren",
"settings_gpxExportAll": "Alle Knoten als GPX exportieren",
"settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.",
"settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren",
"settings_gpxExportContacts": "Kontakte als GPX exportieren",
"settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.",
"settings_gpxExportContactsSubtitle": "Exportiert Begleiter mit einem Ort in eine GPX-Datei.",
"settings_gpxExportContactsSubtitle": "Exportiert Kontakte mit einem Ort in eine GPX-Datei.",
"settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte",
"settings_gpxExportChat": "Begleiterstandorte",
"settings_gpxExportChat": "Kontaktstandorte",
"settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.",
"settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.",
"settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt",
"settings_gpxExportSuccess": "Erfolgreich GPX-Datei exportiert.",
"settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.",
"settings_gpxExportAllContacts": "Alle Kontaktstandorte",
"settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren",
"settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert",
"pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!"
"settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren",
"settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert",
"pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!",
"map_removeLast": "Letztes Entfernen",
"map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.",
"map_runTrace": "Pfadverlauf ausführen",
"pathTrace_clearTooltip": "Pfad löschen",
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
"scanner_enableBluetooth": "Bluetooth aktivieren",
"snrIndicator_lastSeen": "Zuletzt gesehen",
"snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater",
"chat_ShowAllPaths": "Alle Pfade anzeigen",
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.",
"settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Einheiten",
"appSettings_unitsMetric": "Metrisch (m/km)",
"appSettings_unitsImperial": "Imperial (ft/mi)",
"map_lineOfSight": "Sichtlinie",
"map_losScreenTitle": "Sichtlinie",
"losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.",
"losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Löschen Sie alle Punkte",
"losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen",
"losMenuTitle": "LOS-Menü",
"losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen",
"losShowDisplayNodes": "Anzeigeknoten anzeigen",
"losCustomPoints": "Benutzerdefinierte Punkte",
"losCustomPointLabel": "Benutzerdefiniert {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antenne A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Führen Sie LOS aus",
"losNoElevationData": "Keine Höhendaten",
"losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blockiert durch {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: Überprüfen...",
"losStatusNoData": "LOS: keine Daten",
"losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.",
"losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.",
"losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um",
"losPointName": "Punktname",
"losShowPanelTooltip": "LOS-Panel anzeigen",
"losHidePanelTooltip": "LOS-Panel ausblenden",
"losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Funkhorizont",
"losLegendLosBeam": "Sichtlinie",
"losLegendTerrain": "Gelände",
"losFrequencyLabel": "Frequenz",
"losFrequencyInfoTooltip": "Details zur Berechnung anzeigen",
"losFrequencyDialogTitle": "Berechnung des Funkhorizonts",
"losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Ungelesen",
"contacts_searchContactsNoNumber": "Kontakte suchen...",
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
}
+623 -173
View File
File diff suppressed because it is too large Load Diff
+230 -16
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "es",
"appTitle": "MeshCore Open",
"nav_contacts": "Contactos",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Número de contactos",
"settings_infoChannelCount": "Número de canales",
"settings_presets": "Preajustes",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frecuencia (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Potencia (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)",
"settings_longRange": "Largo Alcance",
"settings_fastSpeed": "Velocidad Rápida",
"settings_error": "Error: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Canal público",
"channels_privateChannel": "Canal privado",
"channels_editChannel": "Editar canal",
"channels_muteChannel": "Silenciar canal",
"channels_unmuteChannel": "Activar canal",
"channels_deleteChannel": "Eliminar canal",
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighbours": "Vecinos",
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
"repeater_neighbors": "Vecinos",
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
"neighbors_receivedData": "Recibidas Datos de Vecinos",
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
"neighbors_noData": "No hay datos de vecinos disponibles.",
"channels_joinPrivateChannel": "Únete a un Canal Privado",
"channels_createPrivateChannel": "Crear un Canal Privado",
@@ -1556,6 +1561,8 @@
"appSettings_languageUk": "Ucraniano",
"contacts_clipboardEmpty": "El portapapeles está vacío.",
"appSettings_languageRu": "Ruso",
"appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes",
"appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes",
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
"contacts_floodAdvert": "Anuncio de inundación",
"contacts_contactImported": "El contacto ha sido importado.",
@@ -1569,34 +1576,40 @@
"contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.",
"contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.",
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
"notification_activityTitle": "Actividad de MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}",
"@notification_messagesCount": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}",
"@notification_channelMessagesCount": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}",
"@notification_newNodesCount": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"notification_newTypeDiscovered": "Nuevo {contactType} descubierto",
"@notification_newTypeDiscovered": {
"placeholders": {
"contactType": {"type": "String"}
"contactType": {
"type": "String"
}
}
},
"notification_receivedNewMessage": "Nuevo mensaje recibido",
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
"settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.",
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX",
"settings_gpxExportSuccess": "Archivo GPX exportado con éxito.",
@@ -1612,6 +1625,207 @@
"settings_gpxExportAllContacts": "Todas las ubicaciones de contactos",
"settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX",
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación"
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación",
"pathTrace_clearTooltip": "Borrar ruta",
"map_runTrace": "Ejecutar Rastreo de Ruta",
"map_tapToAdd": "Pulse en los nodos para agregarlos al camino.",
"map_removeLast": "Eliminar último",
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
"scanner_bluetoothOff": "Bluetooth está desactivado.",
"scanner_enableBluetooth": "Habilitar Bluetooth",
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
"snrIndicator_lastSeen": "Visto por última vez",
"chat_ShowAllPaths": "Mostrar todos los caminos",
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
"settings_clientRepeat": "Repetir sin conexión",
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.",
"settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unidades",
"appSettings_unitsMetric": "Métrico (m/km)",
"appSettings_unitsImperial": "Imperial (pies/millas)",
"map_lineOfSight": "Línea de visión",
"map_losScreenTitle": "Línea de visión",
"losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.",
"losRunFailed": "Error en la comprobación de la línea de visión: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Borrar todos los puntos",
"losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación",
"losMenuTitle": "Menú LOS",
"losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados",
"losShowDisplayNodes": "Mostrar nodos de visualización",
"losCustomPoints": "Puntos personalizados",
"losCustomPointLabel": "Personalizado {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punto A",
"losPointB": "Punto B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Ejecutar LOS",
"losNoElevationData": "Sin datos de elevación",
"losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: comprobando...",
"losStatusNoData": "LOS: sin datos",
"losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.",
"losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.",
"losRenameCustomPoint": "Cambiar el nombre del punto personalizado",
"losPointName": "Nombre del punto",
"losShowPanelTooltip": "Mostrar panel LOS",
"losHidePanelTooltip": "Ocultar panel LOS",
"losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizonte radioeléctrico",
"losLegendLosBeam": "Línea de visión",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frecuencia",
"losFrequencyInfoTooltip": "Ver detalles del cálculo",
"losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico",
"losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_favorites": "Favoritos",
"listFilter_removeFromFavorites": "Eliminar de las favoritas",
"listFilter_addToFavorites": "Añadir a favoritos",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchContactsNoNumber": "Buscar contactos...",
"contacts_unread": "No leído",
"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..."
}
+255 -48
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "fr",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacts",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Nombre de contacts",
"settings_infoChannelCount": "Nombre de canaux",
"settings_presets": "Préréglages",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Fréquence (MHz)",
"settings_frequencyHelper": "300,0 - 2 500,0",
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Puissance (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
"settings_longRange": "Portée Longue",
"settings_fastSpeed": "Vitesse Rapide",
"settings_error": "Erreur : {message}",
"@settings_error": {
"placeholders": {
@@ -210,8 +213,8 @@
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Affichage de la carte",
"appSettings_showRepeaters": "Afficher les répétiteurs",
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répétiteurs sur la carte",
"appSettings_showRepeaters": "Afficher les répéteurs",
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte",
"appSettings_showChatNodes": "Afficher les nœuds de discussion",
"appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte",
"appSettings_showOtherNodes": "Afficher d'autres nœuds",
@@ -266,8 +269,8 @@
}
}
},
"contacts_manageRepeater": "Gérer le répétiteur",
"contacts_roomLogin": "Connexion Salle",
"contacts_manageRepeater": "Gérer le répéteur",
"contacts_roomLogin": "Connexion Room Server",
"contacts_openChat": "Ouverture du Chat",
"contacts_editGroup": "Modifier le groupe",
"contacts_deleteGroup": "Supprimer le groupe",
@@ -339,6 +342,8 @@
"channels_publicChannel": "Canal public",
"channels_privateChannel": "Canal privé",
"channels_editChannel": "Modifier le canal",
"channels_muteChannel": "Désactiver les notifications du canal",
"channels_unmuteChannel": "Réactiver les notifications du canal",
"channels_deleteChannel": "Supprimer le canal",
"channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
"@channels_deleteChannelConfirm": {
@@ -542,9 +547,9 @@
"chat_forceFloodMode": "Mode tout le réseau forcé",
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
"chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
"chat_hopSingular": "Sautez",
"chat_hopPlural": "sautez",
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
"chat_hopSingular": "saut",
"chat_hopPlural": "sauts",
"chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}",
"@chat_hopsCount": {
"placeholders": {
"count": {
@@ -636,7 +641,7 @@
}
},
"map_chat": "Chat",
"map_repeater": "Répétiteur",
"map_repeater": "Répéteur",
"map_room": "Salle",
"map_sensor": "Capteur",
"map_pinDm": "Clé (DM)",
@@ -677,7 +682,7 @@
"map_lastSeenTime": "Dernière fois vu",
"map_sharedPin": "Clé partagée",
"map_joinRoom": "Rejoindre la salle",
"map_manageRepeater": "Gérer le répétiteur",
"map_manageRepeater": "Gérer le répéteur",
"mapCache_title": "Cache de Carte Hors Ligne",
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
"mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.",
@@ -800,13 +805,13 @@
"time_allTime": "Tout le temps",
"dialog_disconnect": "Déconnecter",
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
"login_repeaterLogin": "Connexion au répétiteur",
"login_roomLogin": "Connexion Salle",
"login_repeaterLogin": "Connexion au répéteur",
"login_roomLogin": "Connexion Room Server",
"login_password": "Mot de passe",
"login_enterPassword": "Entrez votre mot de passe",
"login_savePassword": "Sauvegarder le mot de passe",
"login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.",
"login_repeaterDescription": "Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l'état.",
"login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.",
"login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.",
"login_routing": "Redirection",
"login_routingMode": "Mode de routage",
@@ -871,17 +876,17 @@
},
"path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.",
"path_setPath": "Définir le chemin",
"repeater_management": "Gestion des répétiteurs",
"repeater_management": "Gestion des répéteurs",
"repeater_managementTools": "Outils de Gestion",
"repeater_status": "État",
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur",
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur",
"repeater_telemetry": "Télémetrie",
"repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Envoyer des commandes au répétiteur",
"repeater_cliSubtitle": "Envoyer des commandes au répéteur",
"repeater_settings": "Paramètres",
"repeater_settingsSubtitle": "Configurer les paramètres du répétiteur",
"repeater_statusTitle": "État du répétiteur",
"repeater_settingsSubtitle": "Configurer les paramètres du répéteur",
"repeater_statusTitle": "État du répéteur",
"repeater_routingMode": "Mode de routage",
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"repeater_forceFloodMode": "Mode tout le réseau forcé",
@@ -976,10 +981,10 @@
}
}
},
"repeater_settingsTitle": "Paramètres du répétiteur",
"repeater_settingsTitle": "Paramètres du répéteur",
"repeater_basicSettings": "Paramètres de base",
"repeater_repeaterName": "Nom du répétiteur",
"repeater_repeaterNameHelper": "Afficher le nom de ce répétiteur",
"repeater_repeaterName": "Nom du répéteur",
"repeater_repeaterNameHelper": "Afficher le nom de ce répéteur",
"repeater_adminPassword": "Mot de passe Administrateur",
"repeater_adminPasswordHelper": "Mot de passe d'accès complet",
"repeater_guestPassword": "Mot de passe invité",
@@ -999,7 +1004,7 @@
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
"repeater_features": "Fonctionnalités",
"repeater_packetForwarding": "Transfert de paquets",
"repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets",
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
"repeater_guestAccess": "Accès Invité",
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
"repeater_privacyMode": "Mode de confidentialité",
@@ -1026,14 +1031,14 @@
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
"repeater_dangerZone": "Zone dangereuse",
"repeater_rebootRepeater": "Redémarrer Répéteur",
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur",
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?",
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur",
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?",
"repeater_regenerateIdentityKey": "Ré générer la clé d'identité",
"repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée",
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répétiteur. Continuer ?",
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?",
"repeater_eraseFileSystem": "Supprimer le système de fichiers",
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répétiteur",
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !",
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur",
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !",
"repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.",
"repeater_commandSent": "Commande envoyée : {command}",
"@repeater_commandSent": {
@@ -1085,7 +1090,7 @@
}
}
},
"repeater_cliTitle": "Répétiteur CLI",
"repeater_cliTitle": "Répéteur CLI",
"repeater_debugNextCommand": "Déboguer Prochaine Commande",
"repeater_commandHelp": "Aide",
"repeater_clearHistory": "Effacer l'historique",
@@ -1119,13 +1124,13 @@
"repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.",
"repeater_cliHelpSetAf": "Définit le facteur de temps d'air.",
"repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).",
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.",
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.",
"repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)",
"repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).",
"repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.",
"repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.",
"repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».",
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.",
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.",
"repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.",
"repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")",
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
@@ -1147,7 +1152,7 @@
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
"repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.",
"repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
"repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
@@ -1171,8 +1176,8 @@
"repeater_settingsCategory": "Paramètres",
"repeater_bridge": "Pont",
"repeater_logging": "Journalisation",
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répétiteur)",
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répétiteur)",
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)",
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)",
"repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.",
"repeater_gpsManagement": "Gestion GPS",
"repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.",
@@ -1241,7 +1246,7 @@
"channelPath_title": "Chemin de paquet",
"channelPath_viewMap": "Afficher la carte",
"channelPath_otherObservedPaths": "Autres chemins observés",
"channelPath_repeaterHops": "Sauts du répétiteur",
"channelPath_repeaterHops": "Sauts du répéteur",
"channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.",
"channelPath_messageDetails": "Détails du message",
"channelPath_senderLabel": "Expéditeur",
@@ -1306,7 +1311,7 @@
}
},
"channelPath_mapTitle": "Carte du chemin",
"channelPath_noRepeaterLocations": "Aucune position de répétiteur disponible pour ce chemin.",
"channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.",
"channelPath_primaryPath": "Chemin {index} (Principal)",
"@channelPath_primaryPath": {
"placeholders": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighbours": "Voisins",
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
"repeater_neighbors": "Voisins",
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
"neighbors_receivedData": "Données des voisins reçues",
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
"neighbors_repeatersNeighbors": "Répéteurs Voisins",
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
@@ -1396,7 +1401,7 @@
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
"contacts_manageRoom": "Gérer le Room Server",
"room_management": "Administración del Servidor de Habitación",
"room_management": "Administrattion Room Server",
"@community_joinConfirmation": {
"placeholders": {
"name": {
@@ -1556,11 +1561,13 @@
"contacts_invalidAdvertFormat": "Données de contact non valides",
"appSettings_languageUk": "Ukrainien",
"appSettings_languageRu": "Russe",
"appSettings_enableMessageTracing": "Activer le traçage des messages",
"appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages",
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
"contacts_contactImported": "Le contact a été importé.",
"contacts_floodAdvert": "Annonce de crue",
"contacts_floodAdvert": "Annonce à tout le réseau",
"contacts_contactImportFailed": "Échec de l'importation du contact.",
"contacts_zeroHopAdvert": "Annonce Zero Hop",
"contacts_zeroHopAdvert": "Annonce Zero saut",
"contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers",
"contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers",
"contacts_ShareContact": "Copier le contact dans le presse-papiers",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
"notification_receivedNewMessage": "Nouveau message reçu",
"contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.",
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportAllContacts": "Tous les emplacements des contacts",
"settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX",
"pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !"
"pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !",
"map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.",
"pathTrace_clearTooltip": "Effacer le chemin",
"map_pathTraceCancelled": "Traçage de chemin annulé",
"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_bluetoothOff": "Le Bluetooth est désactivé.",
"scanner_enableBluetooth": "Activer le Bluetooth",
"snrIndicator_lastSeen": "Dernière fois vu",
"snrIndicator_nearByRepeaters": "Répéteurs à proximité",
"chat_ShowAllPaths": "Afficher tous les chemins",
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
"settings_clientRepeat": "Répétition hors réseau",
"settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unités",
"appSettings_unitsMetric": "Métrique (m/km)",
"appSettings_unitsImperial": "Impérial (ft / mi)",
"map_lineOfSight": "Ligne de vue",
"map_losScreenTitle": "Ligne de vue",
"losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.",
"losRunFailed": "Échec de la vérification de la ligne de vue : {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Effacer tous les points",
"losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés",
"losShowDisplayNodes": "Afficher les nœuds d'affichage",
"losCustomPoints": "Points personnalisés",
"losCustomPointLabel": "Personnalisé {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Point A",
"losPointB": "Point B",
"losAntennaA": "Antenne A : {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B : {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Exécuter la LOS",
"losNoElevationData": "Aucune donnée d'altitude",
"losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS : vérification...",
"losStatusNoData": "LOS : aucune donnée",
"losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.",
"losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.",
"losRenameCustomPoint": "Renommer le point personnalisé",
"losPointName": "Nom du point",
"losShowPanelTooltip": "Afficher le panneau LOS",
"losHidePanelTooltip": "Masquer le panneau LOS",
"losElevationAttribution": "Données daltitude : Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizon radio",
"losLegendLosBeam": "Ligne de visée",
"losLegendTerrain": "Terrain",
"losFrequencyLabel": "Fréquence",
"losFrequencyInfoTooltip": "Voir les détails du calcul",
"losFrequencyDialogTitle": "Calcul de lhorizon radio",
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Ajouter à mes favoris",
"listFilter_removeFromFavorites": "Supprimer des favoris",
"listFilter_favorites": "Préférences",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Non lu",
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "it",
"appTitle": "MeshCore Open",
"nav_contacts": "Contatti",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Numero contatti",
"settings_infoChannelCount": "Numero Canale",
"settings_presets": "Preset",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequenza (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Potenza (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)",
"settings_longRange": "Lungo Raggio",
"settings_fastSpeed": "Velocità Rapida",
"settings_error": "Errore: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Canale pubblico",
"channels_privateChannel": "Canale privato",
"channels_editChannel": "Modifica canale",
"channels_muteChannel": "Silenzia canale",
"channels_unmuteChannel": "Attiva notifiche canale",
"channels_deleteChannel": "Elimina canale",
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighbours": "Vicini",
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
"repeater_neighbors": "Vicini",
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
"neighbors_receivedData": "Ricevute dati vicini",
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
"channels_createPrivateChannel": "Crea un Canale Privato",
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
@@ -1556,6 +1561,8 @@
"appSettings_languageRu": "Russo",
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
"appSettings_languageUk": "Ucraino",
"appSettings_enableMessageTracing": "Abilita tracciamento messaggi",
"appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi",
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
"contacts_floodAdvert": "Annuncio alluvionale",
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
"notification_newTypeDiscovered": "Nuovo {contactType} scoperto",
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
"settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX",
"settings_gpxExportContacts": "Esporta compagni in GPX",
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!"
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
"map_removeLast": "Rimuovi ultimo",
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
"pathTrace_clearTooltip": "Pulisci percorso",
"map_runTrace": "Esegui Path Trace",
"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_enableBluetooth": "Abilita il Bluetooth",
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
"snrIndicator_lastSeen": "Ultimo accesso",
"chat_ShowAllPaths": "Mostra tutti i percorsi",
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unità",
"appSettings_unitsMetric": "Metrico (m/km)",
"appSettings_unitsImperial": "Imperiale (ft / mi)",
"map_lineOfSight": "Linea di vista",
"map_losScreenTitle": "Linea di vista",
"losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.",
"losRunFailed": "Controllo della linea di vista fallito: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Cancella tutti i punti",
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
"losMenuTitle": "Menù LOS",
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
"losCustomPoints": "Punti personalizzati",
"losCustomPointLabel": "Personalizzato {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punto A",
"losPointB": "Punto B",
"losAntennaA": "Antenna A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenna B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Esegui LOS",
"losNoElevationData": "Nessun dato di elevazione",
"losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: controllo...",
"losStatusNoData": "LOS: nessun dato",
"losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
"losRenameCustomPoint": "Rinomina punto personalizzato",
"losPointName": "Nome del punto",
"losShowPanelTooltip": "Mostra il pannello LOS",
"losHidePanelTooltip": "Nascondi il pannello LOS",
"losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Orizzonte radio",
"losLegendLosBeam": "Linea di vista",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequenza",
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
"losFrequencyDialogTitle": "Calcolo dellorizzonte radio",
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Aggiungi ai preferiti",
"listFilter_removeFromFavorites": "Rimuovi dai preferiti",
"listFilter_favorites": "Preferiti",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchUsers": "Cerca {number}{str} Utenti...",
"contacts_searchContactsNoNumber": "Cerca Contatti...",
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
"contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
}
+429 -36
View File
@@ -376,6 +376,24 @@ abstract class AppLocalizations {
/// **'Scan'**
String get scanner_scan;
/// No description provided for @scanner_bluetoothOff.
///
/// In en, this message translates to:
/// **'Bluetooth is off'**
String get scanner_bluetoothOff;
/// No description provided for @scanner_bluetoothOffMessage.
///
/// In en, this message translates to:
/// **'Please turn on Bluetooth to scan for devices'**
String get scanner_bluetoothOffMessage;
/// No description provided for @scanner_enableBluetooth.
///
/// In en, this message translates to:
/// **'Enable Bluetooth'**
String get scanner_enableBluetooth;
/// No description provided for @device_quickSwitch.
///
/// In en, this message translates to:
@@ -682,6 +700,12 @@ abstract class AppLocalizations {
/// **'An open-source Flutter client for MeshCore LoRa mesh networking devices.'**
String get settings_aboutDescription;
/// No description provided for @settings_aboutOpenMeteoAttribution.
///
/// In en, this message translates to:
/// **'LOS elevation data: Open-Meteo (CC BY 4.0)'**
String get settings_aboutOpenMeteoAttribution;
/// No description provided for @settings_infoName.
///
/// In en, this message translates to:
@@ -730,24 +754,6 @@ abstract class AppLocalizations {
/// **'Presets'**
String get settings_presets;
/// No description provided for @settings_preset915Mhz.
///
/// In en, this message translates to:
/// **'915 MHz'**
String get settings_preset915Mhz;
/// No description provided for @settings_preset868Mhz.
///
/// In en, this message translates to:
/// **'868 MHz'**
String get settings_preset868Mhz;
/// No description provided for @settings_preset433Mhz.
///
/// In en, this message translates to:
/// **'433 MHz'**
String get settings_preset433Mhz;
/// No description provided for @settings_frequency.
///
/// In en, this message translates to:
@@ -802,17 +808,23 @@ abstract class AppLocalizations {
/// **'Invalid TX power (0-22 dBm)'**
String get settings_txPowerInvalid;
/// No description provided for @settings_longRange.
/// No description provided for @settings_clientRepeat.
///
/// In en, this message translates to:
/// **'Long Range'**
String get settings_longRange;
/// **'Off-Grid Repeat'**
String get settings_clientRepeat;
/// No description provided for @settings_fastSpeed.
/// No description provided for @settings_clientRepeatSubtitle.
///
/// In en, this message translates to:
/// **'Fast Speed'**
String get settings_fastSpeed;
/// **'Allow this device to repeat mesh packets for others'**
String get settings_clientRepeatSubtitle;
/// No description provided for @settings_clientRepeatFreqWarning.
///
/// In en, this message translates to:
/// **'Off-grid repeat requires 433, 869, or 918 MHz frequency'**
String get settings_clientRepeatFreqWarning;
/// No description provided for @settings_error.
///
@@ -958,6 +970,18 @@ abstract class AppLocalizations {
/// **'Українська'**
String get appSettings_languageUk;
/// No description provided for @appSettings_enableMessageTracing.
///
/// In en, this message translates to:
/// **'Enable Message Tracing'**
String get appSettings_enableMessageTracing;
/// No description provided for @appSettings_enableMessageTracingSubtitle.
///
/// In en, this message translates to:
/// **'Show detailed routing and timing metadata for messages'**
String get appSettings_enableMessageTracingSubtitle;
/// No description provided for @appSettings_notifications.
///
/// In en, this message translates to:
@@ -1234,6 +1258,24 @@ abstract class AppLocalizations {
/// **'Offline Map Cache'**
String get appSettings_offlineMapCache;
/// No description provided for @appSettings_unitsTitle.
///
/// In en, this message translates to:
/// **'Units'**
String get appSettings_unitsTitle;
/// No description provided for @appSettings_unitsMetric.
///
/// In en, this message translates to:
/// **'Metric (m / km)'**
String get appSettings_unitsMetric;
/// No description provided for @appSettings_unitsImperial.
///
/// In en, this message translates to:
/// **'Imperial (ft / mi)'**
String get appSettings_unitsImperial;
/// No description provided for @appSettings_noAreaSelected.
///
/// In en, this message translates to:
@@ -1294,11 +1336,47 @@ abstract class AppLocalizations {
/// **'Contacts will appear when devices advertise'**
String get contacts_contactsWillAppear;
/// No description provided for @contacts_unread.
///
/// In en, this message translates to:
/// **'Unread'**
String get contacts_unread;
/// No description provided for @contacts_searchContactsNoNumber.
///
/// In en, this message translates to:
/// **'Search Contacts...'**
String get contacts_searchContactsNoNumber;
/// No description provided for @contacts_searchContacts.
///
/// In en, this message translates to:
/// **'Search contacts...'**
String get contacts_searchContacts;
/// **'Search {number}{str} Contacts...'**
String contacts_searchContacts(int number, String str);
/// No description provided for @contacts_searchFavorites.
///
/// In en, this message translates to:
/// **'Search {number}{str} Favorites...'**
String contacts_searchFavorites(int number, String str);
/// No description provided for @contacts_searchUsers.
///
/// In en, this message translates to:
/// **'Search {number}{str} Users...'**
String contacts_searchUsers(int number, String str);
/// No description provided for @contacts_searchRepeaters.
///
/// In en, this message translates to:
/// **'Search {number}{str} Repeaters...'**
String contacts_searchRepeaters(int number, String str);
/// No description provided for @contacts_searchRoomServers.
///
/// In en, this message translates to:
/// **'Search {number}{str} Room servers...'**
String contacts_searchRoomServers(int number, String str);
/// No description provided for @contacts_noUnreadContacts.
///
@@ -1516,6 +1594,18 @@ abstract class AppLocalizations {
/// **'Edit channel'**
String get channels_editChannel;
/// No description provided for @channels_muteChannel.
///
/// In en, this message translates to:
/// **'Mute channel'**
String get channels_muteChannel;
/// No description provided for @channels_unmuteChannel.
///
/// In en, this message translates to:
/// **'Unmute channel'**
String get channels_unmuteChannel;
/// No description provided for @channels_deleteChannel.
///
/// In en, this message translates to:
@@ -1528,6 +1618,12 @@ abstract class AppLocalizations {
/// **'Delete \"{name}\"? This cannot be undone.'**
String channels_deleteChannelConfirm(String name);
/// No description provided for @channels_channelDeleteFailed.
///
/// In en, this message translates to:
/// **'Failed to delete channel \"{name}\"'**
String channels_channelDeleteFailed(String name);
/// No description provided for @channels_channelDeleted.
///
/// In en, this message translates to:
@@ -2026,6 +2122,12 @@ abstract class AppLocalizations {
/// **'Path Management'**
String get chat_pathManagement;
/// No description provided for @chat_ShowAllPaths.
///
/// In en, this message translates to:
/// **'Show all paths'**
String get chat_ShowAllPaths;
/// No description provided for @chat_routingMode.
///
/// In en, this message translates to:
@@ -2278,6 +2380,18 @@ abstract class AppLocalizations {
/// **'Node Map'**
String get map_title;
/// No description provided for @map_lineOfSight.
///
/// In en, this message translates to:
/// **'Line of Sight'**
String get map_lineOfSight;
/// No description provided for @map_losScreenTitle.
///
/// In en, this message translates to:
/// **'Line of Sight'**
String get map_losScreenTitle;
/// No description provided for @map_noNodesWithLocation.
///
/// In en, this message translates to:
@@ -2518,6 +2632,30 @@ abstract class AppLocalizations {
/// **'Manage Repeater'**
String get map_manageRepeater;
/// No description provided for @map_tapToAdd.
///
/// In en, this message translates to:
/// **'Tap on nodes to add them to the path.'**
String get map_tapToAdd;
/// No description provided for @map_runTrace.
///
/// In en, this message translates to:
/// **'Run Path Trace'**
String get map_runTrace;
/// No description provided for @map_removeLast.
///
/// In en, this message translates to:
/// **'Remove Last'**
String get map_removeLast;
/// No description provided for @map_pathTraceCancelled.
///
/// In en, this message translates to:
/// **'Path trace cancelled.'**
String get map_pathTraceCancelled;
/// No description provided for @mapCache_title.
///
/// In en, this message translates to:
@@ -2997,17 +3135,17 @@ abstract class AppLocalizations {
/// **'Send commands to the repeater'**
String get repeater_cliSubtitle;
/// No description provided for @repeater_neighbours.
/// No description provided for @repeater_neighbors.
///
/// In en, this message translates to:
/// **'Neighbors'**
String get repeater_neighbours;
String get repeater_neighbors;
/// No description provided for @repeater_neighboursSubtitle.
/// No description provided for @repeater_neighborsSubtitle.
///
/// In en, this message translates to:
/// **'View zero hop neighbors.'**
String get repeater_neighboursSubtitle;
String get repeater_neighborsSubtitle;
/// No description provided for @repeater_settings.
///
@@ -4151,13 +4289,13 @@ abstract class AppLocalizations {
/// No description provided for @neighbors_receivedData.
///
/// In en, this message translates to:
/// **'Received Neighbours Data'**
/// **'Received Neighbors Data'**
String get neighbors_receivedData;
/// No description provided for @neighbors_requestTimedOut.
///
/// In en, this message translates to:
/// **'Neighbours request timed out.'**
/// **'Neighbors request timed out.'**
String get neighbors_requestTimedOut;
/// No description provided for @neighbors_errorLoading.
@@ -4166,16 +4304,16 @@ abstract class AppLocalizations {
/// **'Error loading neighbors: {error}'**
String neighbors_errorLoading(String error);
/// No description provided for @neighbors_repeatersNeighbours.
/// No description provided for @neighbors_repeatersNeighbors.
///
/// In en, this message translates to:
/// **'Repeaters Neighbours'**
String get neighbors_repeatersNeighbours;
/// **'Repeaters Neighbors'**
String get neighbors_repeatersNeighbors;
/// No description provided for @neighbors_noData.
///
/// In en, this message translates to:
/// **'No neighbours data available.'**
/// **'No neighbors data available.'**
String get neighbors_noData;
/// No description provided for @neighbors_unknownContact.
@@ -4670,6 +4808,24 @@ abstract class AppLocalizations {
/// **'All'**
String get listFilter_all;
/// No description provided for @listFilter_favorites.
///
/// In en, this message translates to:
/// **'Favorites'**
String get listFilter_favorites;
/// No description provided for @listFilter_addToFavorites.
///
/// In en, this message translates to:
/// **'Add to favorites'**
String get listFilter_addToFavorites;
/// No description provided for @listFilter_removeFromFavorites.
///
/// In en, this message translates to:
/// **'Remove from favorites'**
String get listFilter_removeFromFavorites;
/// No description provided for @listFilter_users.
///
/// In en, this message translates to:
@@ -4730,6 +4886,231 @@ abstract class AppLocalizations {
/// **'One or more of the hops is missing a location!'**
String get pathTrace_someHopsNoLocation;
/// No description provided for @pathTrace_clearTooltip.
///
/// In en, this message translates to:
/// **'Clear path.'**
String get pathTrace_clearTooltip;
/// No description provided for @losSelectStartEnd.
///
/// In en, this message translates to:
/// **'Select start and end nodes for LOS.'**
String get losSelectStartEnd;
/// No description provided for @losRunFailed.
///
/// In en, this message translates to:
/// **'Line-of-sight check failed: {error}'**
String losRunFailed(String error);
/// No description provided for @losClearAllPoints.
///
/// In en, this message translates to:
/// **'Clear all points'**
String get losClearAllPoints;
/// No description provided for @losRunToViewElevationProfile.
///
/// In en, this message translates to:
/// **'Run LOS to view elevation profile'**
String get losRunToViewElevationProfile;
/// No description provided for @losMenuTitle.
///
/// In en, this message translates to:
/// **'LOS Menu'**
String get losMenuTitle;
/// No description provided for @losMenuSubtitle.
///
/// In en, this message translates to:
/// **'Tap nodes or long-press map for custom points'**
String get losMenuSubtitle;
/// No description provided for @losShowDisplayNodes.
///
/// In en, this message translates to:
/// **'Show display nodes'**
String get losShowDisplayNodes;
/// No description provided for @losCustomPoints.
///
/// In en, this message translates to:
/// **'Custom points'**
String get losCustomPoints;
/// No description provided for @losCustomPointLabel.
///
/// In en, this message translates to:
/// **'Custom {index}'**
String losCustomPointLabel(int index);
/// No description provided for @losPointA.
///
/// In en, this message translates to:
/// **'Point A'**
String get losPointA;
/// No description provided for @losPointB.
///
/// In en, this message translates to:
/// **'Point B'**
String get losPointB;
/// No description provided for @losAntennaA.
///
/// In en, this message translates to:
/// **'Antenna A: {value} {unit}'**
String losAntennaA(String value, String unit);
/// No description provided for @losAntennaB.
///
/// In en, this message translates to:
/// **'Antenna B: {value} {unit}'**
String losAntennaB(String value, String unit);
/// No description provided for @losRun.
///
/// In en, this message translates to:
/// **'Run LOS'**
String get losRun;
/// No description provided for @losNoElevationData.
///
/// In en, this message translates to:
/// **'No elevation data'**
String get losNoElevationData;
/// No description provided for @losProfileClear.
///
/// In en, this message translates to:
/// **'{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}'**
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
);
/// No description provided for @losProfileBlocked.
///
/// In en, this message translates to:
/// **'{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}'**
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
);
/// No description provided for @losStatusChecking.
///
/// In en, this message translates to:
/// **'LOS: checking...'**
String get losStatusChecking;
/// No description provided for @losStatusNoData.
///
/// In en, this message translates to:
/// **'LOS: no data'**
String get losStatusNoData;
/// No description provided for @losStatusSummary.
///
/// In en, this message translates to:
/// **'LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown'**
String losStatusSummary(int clear, int total, int blocked, int unknown);
/// No description provided for @losErrorElevationUnavailable.
///
/// In en, this message translates to:
/// **'Elevation data unavailable for one or more samples.'**
String get losErrorElevationUnavailable;
/// No description provided for @losErrorInvalidInput.
///
/// In en, this message translates to:
/// **'Invalid points/elevation data for LOS calculation.'**
String get losErrorInvalidInput;
/// No description provided for @losRenameCustomPoint.
///
/// In en, this message translates to:
/// **'Rename custom point'**
String get losRenameCustomPoint;
/// No description provided for @losPointName.
///
/// In en, this message translates to:
/// **'Point name'**
String get losPointName;
/// No description provided for @losShowPanelTooltip.
///
/// In en, this message translates to:
/// **'Show LOS panel'**
String get losShowPanelTooltip;
/// No description provided for @losHidePanelTooltip.
///
/// In en, this message translates to:
/// **'Hide LOS panel'**
String get losHidePanelTooltip;
/// No description provided for @losElevationAttribution.
///
/// In en, this message translates to:
/// **'Elevation data: Open-Meteo (CC BY 4.0)'**
String get losElevationAttribution;
/// No description provided for @losLegendRadioHorizon.
///
/// In en, this message translates to:
/// **'Radio horizon'**
String get losLegendRadioHorizon;
/// No description provided for @losLegendLosBeam.
///
/// In en, this message translates to:
/// **'LOS beam'**
String get losLegendLosBeam;
/// No description provided for @losLegendTerrain.
///
/// In en, this message translates to:
/// **'Terrain'**
String get losLegendTerrain;
/// No description provided for @losFrequencyLabel.
///
/// In en, this message translates to:
/// **'Frequency'**
String get losFrequencyLabel;
/// No description provided for @losFrequencyInfoTooltip.
///
/// In en, this message translates to:
/// **'View calculation details'**
String get losFrequencyInfoTooltip;
/// No description provided for @losFrequencyDialogTitle.
///
/// In en, this message translates to:
/// **'Radio horizon calculation'**
String get losFrequencyDialogTitle;
/// Explain how the calculation uses the baseline frequency and derived k-factor.
///
/// In en, this message translates to:
/// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'**
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
);
/// No description provided for @contacts_pathTrace.
///
/// In en, this message translates to:
@@ -4987,6 +5368,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'meshcore-open GPX map data export'**
String get settings_gpxExportShareSubject;
/// No description provided for @snrIndicator_nearByRepeaters.
///
/// In en, this message translates to:
/// **'Nearby Repeaters'**
String get snrIndicator_nearByRepeaters;
/// No description provided for @snrIndicator_lastSeen.
///
/// In en, this message translates to:
/// **'Last seen'**
String get snrIndicator_lastSeen;
}
class _AppLocalizationsDelegate
+259 -15
View File
@@ -143,6 +143,16 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_scan => 'Сканирай';
@override
String get scanner_bluetoothOff => 'Bluetooth е изключен.';
@override
String get scanner_bluetoothOffMessage =>
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
@override
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
@override
String get device_quickSwitch => 'Бързо превключване';
@@ -316,6 +326,10 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_aboutDescription =>
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Име';
@@ -340,15 +354,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_presets => 'Предварителни настройки';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Честота (MHz)';
@@ -377,10 +382,15 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)';
@override
String get settings_longRange => 'Дълъг обхват';
String get settings_clientRepeat => 'Без електричество – повторение';
@override
String get settings_fastSpeed => 'Бърза скорост';
String get settings_clientRepeatSubtitle =>
'Позволете на това устройство да предава пакети към мрежата за други устройства.';
@override
String get settings_clientRepeatFreqWarning =>
'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.';
@override
String settings_error(String message) {
@@ -456,6 +466,14 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_languageUk => 'Украински';
@override
String get appSettings_enableMessageTracing =>
'Разрешаване на проследяване на съобщения';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения';
@override
String get appSettings_notifications => 'Уведомления';
@@ -616,6 +634,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
@override
String get appSettings_unitsTitle => 'единици';
@override
String get appSettings_unitsMetric => 'Метрика (m / km)';
@override
String get appSettings_unitsImperial => 'Имперска (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Няма избрана област';
@@ -654,7 +681,35 @@ class AppLocalizationsBg extends AppLocalizations {
'Контактите ще се появят, когато устройствата рекламират.';
@override
String get contacts_searchContacts => 'Търсене на контакти...';
String get contacts_unread => 'Непрочетено';
@override
String get contacts_searchContactsNoNumber => 'Търси контакти...';
@override
String contacts_searchContacts(int number, String str) {
return 'Търсене на контакти...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Търсене на $number$str любими...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Търсене на $number$str потребители...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Търсене на $number$str повтарящи се...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Търсене на $number$str сървъри в стаята...';
}
@override
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
@@ -779,6 +834,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channels_editChannel => 'Редактирай канал';
@override
String get channels_muteChannel => 'Заглуши канала';
@override
String get channels_unmuteChannel => 'Включи известията на канала';
@override
String get channels_deleteChannel => 'Изтрий канала';
@@ -787,6 +848,11 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Неуспешно изтриване на канала \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Каналът \"$name\" е изтрит';
@@ -1074,6 +1140,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_pathManagement => 'Управление на пътища';
@override
String get chat_ShowAllPaths => 'Покажи всички пътища';
@override
String get chat_routingMode => 'Режим на маршрутизиране';
@@ -1234,6 +1303,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_title => 'Карта на възлите';
@override
String get map_lineOfSight => 'Линия на видимост';
@override
String get map_losScreenTitle => 'Линия на видимост';
@override
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
@@ -1363,6 +1438,19 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get map_tapToAdd =>
'Натиснете върху възлите, за да ги добавите към пътя.';
@override
String get map_runTrace => 'Изпълни Път на Следване';
@override
String get map_removeLast => 'Премахни Последно';
@override
String get map_pathTraceCancelled => 'Отменен е следването на пътя.';
@override
String get mapCache_title => 'Кеш на офлайн карти';
@@ -1658,10 +1746,10 @@ class AppLocalizationsBg extends AppLocalizations {
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
@override
String get repeater_neighbours => 'Съседи';
String get repeater_neighbors => 'Съседи';
@override
String get repeater_neighboursSubtitle =>
String get repeater_neighborsSubtitle =>
'Преглед на съседни възли с нулев скок.';
@override
@@ -2361,7 +2449,7 @@ class AppLocalizationsBg extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
@override
String get neighbors_noData => 'Няма налични данни за съседи.';
@@ -2668,6 +2756,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get listFilter_all => 'Всички';
@override
String get listFilter_favorites => 'Любими';
@override
String get listFilter_addToFavorites => 'Добави към любими';
@override
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
@override
String get listFilter_users => 'Потребители';
@@ -2699,6 +2796,147 @@ class AppLocalizationsBg extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Един или повече от хмелите липсва местоположение!';
@override
String get pathTrace_clearTooltip => 'Изчисти пътя';
@override
String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.';
@override
String losRunFailed(String error) {
return 'Проверката на пряката видимост е неуспешна: $error';
}
@override
String get losClearAllPoints => 'Изчистете всички точки';
@override
String get losRunToViewElevationProfile =>
'Стартирайте LOS, за да видите профила на надморската височина';
@override
String get losMenuTitle => 'LOS меню';
@override
String get losMenuSubtitle =>
'Докоснете възли или натиснете продължително карта за персонализирани точки';
@override
String get losShowDisplayNodes => 'Показване на възли на дисплея';
@override
String get losCustomPoints => 'Персонализирани точки';
@override
String losCustomPointLabel(int index) {
return 'Персонализирано $index';
}
@override
String get losPointA => 'Точка А';
@override
String get losPointB => 'Точка Б';
@override
String losAntennaA(String value, String unit) {
return 'Антена A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Антена B: $value $unit';
}
@override
String get losRun => 'Стартирайте LOS';
@override
String get losNoElevationData => 'Няма данни за надморска височина';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, блокиран от $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: проверка...';
@override
String get losStatusNoData => 'LOS: няма данни';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно';
}
@override
String get losErrorElevationUnavailable =>
'Няма налични данни за надморска височина за една или повече проби.';
@override
String get losErrorInvalidInput =>
'Невалидни данни за точки/надморска височина за изчисляване на LOS.';
@override
String get losRenameCustomPoint => 'Преименувайте персонализирана точка';
@override
String get losPointName => 'Име на точката';
@override
String get losShowPanelTooltip => 'Показване на LOS панел';
@override
String get losHidePanelTooltip => 'Скриване на LOS панела';
@override
String get losElevationAttribution =>
'Данни за надморска височина: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радиохоризонт';
@override
String get losLegendLosBeam => 'Линия на видимост';
@override
String get losLegendTerrain => 'Терен';
@override
String get losFrequencyLabel => 'Честота';
@override
String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението';
@override
String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.';
}
@override
String get contacts_pathTrace => 'Пътен проследяване';
@@ -2868,4 +3106,10 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open износ на данни за карта в формат GPX';
@override
String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства';
@override
String get snrIndicator_lastSeen => 'Последно видян';
}
+299 -53
View File
@@ -143,6 +143,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanner_scan => 'Scannen';
@override
String get scanner_bluetoothOff => 'Bluetooth ist deaktiviert.';
@override
String get scanner_bluetoothOffMessage =>
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
@override
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
@override
String get device_quickSwitch => 'Schnelles Umschalten';
@@ -244,10 +254,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_actions => 'Aktionen';
@override
String get settings_sendAdvertisement => 'Sende eine Ankündigung';
String get settings_sendAdvertisement => 'Sende Ankündigung';
@override
String get settings_sendAdvertisementSubtitle => 'Sende Ankündigung';
String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung';
@override
String get settings_advertisementSent => 'Ankündigung gesendet';
@@ -267,7 +277,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_refreshContactsSubtitle =>
'Kontakte-Liste vom Gerät neu laden';
'Kontakt-Liste vom Gerät neu laden';
@override
String get settings_rebootDevice => 'Gerät neu starten';
@@ -310,6 +320,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_aboutDescription =>
'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS-Höhendaten: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Name';
@@ -334,15 +348,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_presets => 'Voreinstellungen';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequenz (MHz)';
@@ -371,10 +376,15 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)';
@override
String get settings_longRange => 'Grosse Reichweite';
String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss';
@override
String get settings_fastSpeed => 'Schnelle Geschwindigkeit';
String get settings_clientRepeatSubtitle =>
'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.';
@override
String get settings_clientRepeatFreqWarning =>
'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.';
@override
String settings_error(String message) {
@@ -450,6 +460,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrainisch';
@override
String get appSettings_enableMessageTracing =>
'Nachrichtenverfolgung aktivieren';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen';
@override
String get appSettings_notifications => 'Benachrichtigungen';
@@ -613,6 +631,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
@override
String get appSettings_unitsTitle => 'Einheiten';
@override
String get appSettings_unitsMetric => 'Metrisch (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
@override
String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt';
@@ -650,7 +677,35 @@ class AppLocalizationsDe extends AppLocalizations {
'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.';
@override
String get contacts_searchContacts => 'Suche Kontakte...';
String get contacts_unread => 'Ungelesen';
@override
String get contacts_searchContactsNoNumber => 'Kontakte suchen...';
@override
String contacts_searchContacts(int number, String str) {
return 'Suche Kontakte...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Suche $number$str Favoriten...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Suche $number$str Benutzer...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Suche $number$str Repeater...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Suche $number$str Raumserver...';
}
@override
String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte';
@@ -776,6 +831,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get channels_editChannel => 'Kanal bearbeiten';
@override
String get channels_muteChannel => 'Kanal stummschalten';
@override
String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben';
@override
String get channels_deleteChannel => 'Lösche den Kanal';
@@ -784,6 +845,11 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kanal $name konnte nicht gelöscht werden';
}
@override
String channels_channelDeleted(String name) {
return 'Kanal \"$name\" gelöscht';
@@ -1074,6 +1140,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_pathManagement => 'Pfadverwaltung';
@override
String get chat_ShowAllPaths => 'Alle Pfade anzeigen';
@override
String get chat_routingMode => 'Routenmodus';
@@ -1086,7 +1155,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_recentAckPaths =>
'Aktuelle ACK-Pfade (tasten, um zu verwenden):';
'Aktuelle ACK-Pfade (antippen, um zu verwenden):';
@override
String get chat_pathHistoryFull =>
@@ -1117,7 +1186,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_noPathHistoryYet =>
'Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
@override
String get chat_pathActions => 'Pfadaktionen:';
@@ -1233,6 +1302,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_title => 'Karte';
@override
String get map_lineOfSight => 'Sichtlinie';
@override
String get map_losScreenTitle => 'Sichtlinie';
@override
String get map_noNodesWithLocation => 'Keine Knoten mit Standortdaten';
@@ -1362,6 +1437,19 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_manageRepeater => 'Repeater verwalten';
@override
String get map_tapToAdd =>
'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.';
@override
String get map_runTrace => 'Pfadverlauf ausführen';
@override
String get map_removeLast => 'Letztes Entfernen';
@override
String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.';
@override
String get mapCache_title => 'Offline-Karten-Cache';
@@ -1418,7 +1506,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String mapCache_estimatedTiles(int count) {
return 'Geschätzte Fliesen: $count';
return 'Geschätzte Kacheln: $count';
}
@override
@@ -1592,7 +1680,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get path_hexPrefixInstructions =>
'Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.';
'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.';
@override
String get path_hexPrefixExample =>
@@ -1657,10 +1745,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
@override
String get repeater_neighbours => 'Nachbarn';
String get repeater_neighbors => 'Nachbarn';
@override
String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
String get repeater_neighborsSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
@override
String get repeater_settings => 'Einstellungen';
@@ -1689,7 +1777,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_statusRequestTimeout =>
'Statusanfrage zeitweise fehlgeschlagen.';
'Statusanfrage durch Timeout fehlgeschlagen.';
@override
String repeater_errorLoadingStatus(String error) {
@@ -1766,7 +1854,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String repeater_duplicatesFloodDirect(String flood, String direct) {
return 'Überflut: $flood, Direkt: $direct';
return 'Flut: $flood, Direkt: $direct';
}
@override
@@ -1797,7 +1885,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_guestPasswordHelper =>
'Schreibgeschützter Zugriffspasswort';
'Schreibgeschütztes Zugriffspasswort';
@override
String get repeater_radioSettings => 'Funk Einstellungen';
@@ -1992,7 +2080,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_cliTitle => 'Repeater CLI';
@override
String get repeater_debugNextCommand => 'Fehlersuche Nächster Befehl';
String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls';
@override
String get repeater_commandHelp => 'Hilfe';
@@ -2005,7 +2093,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_typeCommandOrUseQuick =>
'Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle';
'Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle';
@override
String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...';
@@ -2131,7 +2219,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliHelpSetRxDelay =>
'Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
@override
String get repeater_cliHelpSetTxDelay =>
@@ -2175,7 +2263,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliHelpGetBridgeType =>
'Ruft Brückentyp none, rs232, espnow ab.';
'Ruft Brückentyp: none, rs232, espnow ab.';
@override
String get repeater_cliHelpLogStart =>
@@ -2202,7 +2290,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliHelpRegionLoad =>
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.';
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingeckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.';
@override
String get repeater_cliHelpRegionGet =>
@@ -2351,10 +2439,11 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get neighbors_receivedData => 'Empfangene Nachbarendaten';
String get neighbors_receivedData => 'Empfangene Nachbarsdaten';
@override
String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.';
String get neighbors_requestTimedOut =>
'Anfrage durch Timeout fehlgeschlagen.';
@override
String neighbors_errorLoading(String error) {
@@ -2362,19 +2451,19 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Nachbarn';
String get neighbors_repeatersNeighbors => 'Nachbarn';
@override
String get neighbors_noData => 'Keine Nachbardaten verfügbar.';
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Unbekannte $pubkey';
return 'Unbekannt $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Hörte: $time vor her.';
return 'Gehört vor: $time';
}
@override
@@ -2394,7 +2483,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Die Detailangaben für dieses Paket sind nicht verfügbar.';
@override
String get channelPath_messageDetails => 'Nachrichtsdetails';
String get channelPath_messageDetails => 'Nachrichtendetails';
@override
String get channelPath_senderLabel => 'Sender';
@@ -2630,14 +2719,14 @@ class AppLocalizationsDe extends AppLocalizations {
'Füge einen Hashtag-Kanal für diese Community hinzu';
@override
String get community_selectCommunity => 'Wählen Sie Community';
String get community_selectCommunity => 'Wählen Sie eine Community';
@override
String get community_regularHashtag => 'Regulärer Hashtag';
@override
String get community_regularHashtagDesc =>
'Öffentliches Hashtag (jeder kann teilnehmen)';
'Öffentlicher Hashtag (jeder kann teilnehmen)';
@override
String get community_communityHashtag => 'Community Hashtag';
@@ -2672,6 +2761,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get listFilter_all => 'Alle';
@override
String get listFilter_favorites => 'Favoriten';
@override
String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen';
@override
String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen';
@override
String get listFilter_users => 'Benutzer';
@@ -2682,7 +2780,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get listFilter_roomServers => 'Raumserver';
@override
String get listFilter_unreadOnly => 'Nur nicht gelesen';
String get listFilter_unreadOnly => 'Nicht gelesen';
@override
String get listFilter_newGroup => 'Neue Gruppe';
@@ -2701,7 +2799,149 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get pathTrace_someHopsNoLocation =>
'Eine oder mehrere der Hopfen fehlen einen Standort!';
'Bei einer oder mehreren Knoten fehlt der Standort!';
@override
String get pathTrace_clearTooltip => 'Pfad löschen';
@override
String get losSelectStartEnd =>
'Wählen Sie Start- und Endknoten für LOS aus.';
@override
String losRunFailed(String error) {
return 'Sichtlinienprüfung fehlgeschlagen: $error';
}
@override
String get losClearAllPoints => 'Löschen Sie alle Punkte';
@override
String get losRunToViewElevationProfile =>
'Führen Sie LOS aus, um das Höhenprofil anzuzeigen';
@override
String get losMenuTitle => 'LOS-Menü';
@override
String get losMenuSubtitle =>
'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen';
@override
String get losShowDisplayNodes => 'Anzeigeknoten anzeigen';
@override
String get losCustomPoints => 'Benutzerdefinierte Punkte';
@override
String losCustomPointLabel(int index) {
return 'Benutzerdefiniert $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antenne A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenne B: $value $unit';
}
@override
String get losRun => 'Führen Sie LOS aus';
@override
String get losNoElevationData => 'Keine Höhendaten';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, freie Sichtlinie, Mindestabstand $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blockiert durch $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: Überprüfen...';
@override
String get losStatusNoData => 'LOS: keine Daten';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'Sichtlinie: $clear/$total frei, $blocked blockiert, $unknown unbekannt';
}
@override
String get losErrorElevationUnavailable =>
'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.';
@override
String get losErrorInvalidInput =>
'Ungültige Punkte/Höhendaten für die LOS-Berechnung.';
@override
String get losRenameCustomPoint =>
'Benennen Sie den benutzerdefinierten Punkt um';
@override
String get losPointName => 'Punktname';
@override
String get losShowPanelTooltip => 'LOS-Panel anzeigen';
@override
String get losHidePanelTooltip => 'LOS-Panel ausblenden';
@override
String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Funkhorizont';
@override
String get losLegendLosBeam => 'Sichtlinie';
@override
String get losLegendTerrain => 'Gelände';
@override
String get losFrequencyLabel => 'Frequenz';
@override
String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen';
@override
String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.';
}
@override
String get contacts_pathTrace => 'Pfadverfolgung';
@@ -2743,14 +2983,14 @@ class AppLocalizationsDe extends AppLocalizations {
'Kontakt konnte nicht importiert werden';
@override
String get contacts_zeroHopAdvert => 'Zero-Hop-Anzeige';
String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung';
@override
String get contacts_floodAdvert => 'Überflutungsanzeige';
String get contacts_floodAdvert => 'Flut-Ankündigung';
@override
String get contacts_copyAdvertToClipboard =>
'Werbung in die Zwischenablage kopieren';
'Ankündigung in die Zwischenablage kopieren';
@override
String get contacts_addContactFromClipboard =>
@@ -2776,7 +3016,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contacts_contactAdvertCopyFailed =>
'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.';
'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.';
@override
String get notification_activityTitle => 'MeshCore Aktivität';
@@ -2824,28 +3064,28 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_gpxExportRepeaters =>
'Repeater und Raumserver nach GPX exportieren';
'Repeater und Raumserver als GPX exportieren';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.';
@override
String get settings_gpxExportContacts => 'Begleiter nach GPX exportieren';
String get settings_gpxExportContacts => 'Kontakte als GPX exportieren';
@override
String get settings_gpxExportContactsSubtitle =>
'Exportiert Begleiter mit einem Ort in eine GPX-Datei.';
'Exportiert Kontakte mit einem Ort in eine GPX-Datei.';
@override
String get settings_gpxExportAll => 'Alle Kontakte nach GPX exportieren';
String get settings_gpxExportAll => 'Alle Knoten als GPX exportieren';
@override
String get settings_gpxExportAllSubtitle =>
'Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.';
'Exportiert alle Knoten mit einem Standort in eine GPX-Datei.';
@override
String get settings_gpxExportSuccess => 'Erfolgreich GPX-Datei exportiert.';
String get settings_gpxExportSuccess => 'GPX-Datei erfolgreich exportiert.';
@override
String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.';
@@ -2863,16 +3103,22 @@ class AppLocalizationsDe extends AppLocalizations {
'Repeater- und Raumserver-Standorte';
@override
String get settings_gpxExportChat => 'Begleiterstandorte';
String get settings_gpxExportChat => 'Kontaktstandorte';
@override
String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte';
@override
String get settings_gpxExportShareText =>
'Kartendaten aus meshcore-open exportiert';
'GPX-Kartendaten aus meshcore-open exportiert';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open GPX-Kartendaten exportieren';
'GPX-Kartendaten aus meshcore-open exportieren';
@override
String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater';
@override
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
}
+259 -18
View File
@@ -142,6 +142,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_scan => 'Scan';
@override
String get scanner_bluetoothOff => 'Bluetooth is off';
@override
String get scanner_bluetoothOffMessage =>
'Please turn on Bluetooth to scan for devices';
@override
String get scanner_enableBluetooth => 'Enable Bluetooth';
@override
String get device_quickSwitch => 'Quick switch';
@@ -308,6 +318,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_aboutDescription =>
'An open-source Flutter client for MeshCore LoRa mesh networking devices.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS elevation data: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Name';
@@ -332,15 +346,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequency (MHz)';
@@ -369,10 +374,15 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)';
@override
String get settings_longRange => 'Long Range';
String get settings_clientRepeat => 'Off-Grid Repeat';
@override
String get settings_fastSpeed => 'Fast Speed';
String get settings_clientRepeatSubtitle =>
'Allow this device to repeat mesh packets for others';
@override
String get settings_clientRepeatFreqWarning =>
'Off-grid repeat requires 433, 869, or 918 MHz frequency';
@override
String settings_error(String message) {
@@ -448,6 +458,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Show detailed routing and timing metadata for messages';
@override
String get appSettings_notifications => 'Notifications';
@@ -608,6 +625,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Map Cache';
@override
String get appSettings_unitsTitle => 'Units';
@override
String get appSettings_unitsMetric => 'Metric (m / km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft / mi)';
@override
String get appSettings_noAreaSelected => 'No area selected';
@@ -644,7 +670,35 @@ class AppLocalizationsEn extends AppLocalizations {
'Contacts will appear when devices advertise';
@override
String get contacts_searchContacts => 'Search contacts...';
String get contacts_unread => 'Unread';
@override
String get contacts_searchContactsNoNumber => 'Search Contacts...';
@override
String contacts_searchContacts(int number, String str) {
return 'Search $number$str Contacts...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Search $number$str Favorites...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Search $number$str Users...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Search $number$str Repeaters...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Search $number$str Room servers...';
}
@override
String get contacts_noUnreadContacts => 'No unread contacts';
@@ -768,6 +822,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channels_editChannel => 'Edit channel';
@override
String get channels_muteChannel => 'Mute channel';
@override
String get channels_unmuteChannel => 'Unmute channel';
@override
String get channels_deleteChannel => 'Delete channel';
@@ -776,6 +836,11 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Delete \"$name\"? This cannot be undone.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Failed to delete channel \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Channel \"$name\" deleted';
@@ -1059,6 +1124,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get chat_pathManagement => 'Path Management';
@override
String get chat_ShowAllPaths => 'Show all paths';
@override
String get chat_routingMode => 'Routing mode';
@@ -1213,6 +1281,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_title => 'Node Map';
@override
String get map_lineOfSight => 'Line of Sight';
@override
String get map_losScreenTitle => 'Line of Sight';
@override
String get map_noNodesWithLocation => 'No nodes with location data';
@@ -1342,6 +1416,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_manageRepeater => 'Manage Repeater';
@override
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
@override
String get map_runTrace => 'Run Path Trace';
@override
String get map_removeLast => 'Remove Last';
@override
String get map_pathTraceCancelled => 'Path trace cancelled.';
@override
String get mapCache_title => 'Offline Map Cache';
@@ -1632,10 +1718,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get repeater_cliSubtitle => 'Send commands to the repeater';
@override
String get repeater_neighbours => 'Neighbors';
String get repeater_neighbors => 'Neighbors';
@override
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
String get repeater_neighborsSubtitle => 'View zero hop neighbors.';
@override
String get repeater_settings => 'Settings';
@@ -2311,10 +2397,10 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get neighbors_receivedData => 'Received Neighbours Data';
String get neighbors_receivedData => 'Received Neighbors Data';
@override
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
String get neighbors_requestTimedOut => 'Neighbors request timed out.';
@override
String neighbors_errorLoading(String error) {
@@ -2322,10 +2408,10 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
String get neighbors_repeatersNeighbors => 'Repeaters Neighbors';
@override
String get neighbors_noData => 'No neighbours data available.';
String get neighbors_noData => 'No neighbors data available.';
@override
String neighbors_unknownContact(String pubkey) {
@@ -2628,6 +2714,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get listFilter_all => 'All';
@override
String get listFilter_favorites => 'Favorites';
@override
String get listFilter_addToFavorites => 'Add to favorites';
@override
String get listFilter_removeFromFavorites => 'Remove from favorites';
@override
String get listFilter_users => 'Users';
@@ -2659,6 +2754,146 @@ class AppLocalizationsEn extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'One or more of the hops is missing a location!';
@override
String get pathTrace_clearTooltip => 'Clear path.';
@override
String get losSelectStartEnd => 'Select start and end nodes for LOS.';
@override
String losRunFailed(String error) {
return 'Line-of-sight check failed: $error';
}
@override
String get losClearAllPoints => 'Clear all points';
@override
String get losRunToViewElevationProfile =>
'Run LOS to view elevation profile';
@override
String get losMenuTitle => 'LOS Menu';
@override
String get losMenuSubtitle => 'Tap nodes or long-press map for custom points';
@override
String get losShowDisplayNodes => 'Show display nodes';
@override
String get losCustomPoints => 'Custom points';
@override
String losCustomPointLabel(int index) {
return 'Custom $index';
}
@override
String get losPointA => 'Point A';
@override
String get losPointB => 'Point B';
@override
String losAntennaA(String value, String unit) {
return 'Antenna A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenna B: $value $unit';
}
@override
String get losRun => 'Run LOS';
@override
String get losNoElevationData => 'No elevation data';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: checking...';
@override
String get losStatusNoData => 'LOS: no data';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown';
}
@override
String get losErrorElevationUnavailable =>
'Elevation data unavailable for one or more samples.';
@override
String get losErrorInvalidInput =>
'Invalid points/elevation data for LOS calculation.';
@override
String get losRenameCustomPoint => 'Rename custom point';
@override
String get losPointName => 'Point name';
@override
String get losShowPanelTooltip => 'Show LOS panel';
@override
String get losHidePanelTooltip => 'Hide LOS panel';
@override
String get losElevationAttribution =>
'Elevation data: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radio horizon';
@override
String get losLegendLosBeam => 'LOS beam';
@override
String get losLegendTerrain => 'Terrain';
@override
String get losFrequencyLabel => 'Frequency';
@override
String get losFrequencyInfoTooltip => 'View calculation details';
@override
String get losFrequencyDialogTitle => 'Radio horizon calculation';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.';
}
@override
String get contacts_pathTrace => 'Path Trace';
@@ -2824,4 +3059,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open GPX map data export';
@override
String get snrIndicator_nearByRepeaters => 'Nearby Repeaters';
@override
String get snrIndicator_lastSeen => 'Last seen';
}
+260 -15
View File
@@ -143,6 +143,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_scan => 'Escanea';
@override
String get scanner_bluetoothOff => 'Bluetooth está desactivado.';
@override
String get scanner_bluetoothOffMessage =>
'Por favor, active el Bluetooth para escanear dispositivos.';
@override
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
@override
String get device_quickSwitch => 'Cambiar rápidamente';
@@ -313,6 +323,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_aboutDescription =>
'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Datos de elevación LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nombre';
@@ -337,15 +351,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_presets => 'Preajustes';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frecuencia (MHz)';
@@ -374,10 +379,15 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)';
@override
String get settings_longRange => 'Largo Alcance';
String get settings_clientRepeat => 'Repetir sin conexión';
@override
String get settings_fastSpeed => 'Velocidad Rápida';
String get settings_clientRepeatSubtitle =>
'Permita que este dispositivo repita los paquetes de red para otros usuarios.';
@override
String get settings_clientRepeatFreqWarning =>
'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.';
@override
String settings_error(String message) {
@@ -453,6 +463,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ucraniano';
@override
String get appSettings_enableMessageTracing =>
'Habilitar seguimiento de mensajes';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes';
@override
String get appSettings_notifications => 'Notificaciones';
@@ -614,6 +632,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Caché de Mapa Offline';
@override
String get appSettings_unitsTitle => 'Unidades';
@override
String get appSettings_unitsMetric => 'Métrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (pies/millas)';
@override
String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área';
@@ -651,7 +678,35 @@ class AppLocalizationsEs extends AppLocalizations {
'Los contactos aparecerán cuando los dispositivos anuncien.';
@override
String get contacts_searchContacts => 'Buscar contactos...';
String get contacts_unread => 'No leído';
@override
String get contacts_searchContactsNoNumber => 'Buscar contactos...';
@override
String contacts_searchContacts(int number, String str) {
return 'Buscar contactos...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Buscar $number$str Favoritos...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Buscar $number$str Usuarios...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Buscar $number$str Repetidores...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Buscar $number$str servidores de sala...';
}
@override
String get contacts_noUnreadContacts => 'No contactos sin leer';
@@ -777,6 +832,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channels_editChannel => 'Editar canal';
@override
String get channels_muteChannel => 'Silenciar canal';
@override
String get channels_unmuteChannel => 'Activar canal';
@override
String get channels_deleteChannel => 'Eliminar canal';
@@ -785,6 +846,11 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Eliminar \"$name\"? Esto no se puede deshacer.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'No se pudo eliminar el canal \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canal \"$name\" eliminado';
@@ -1073,6 +1139,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestión de Rutas';
@override
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
@override
String get chat_routingMode => 'Modo de enrutamiento';
@@ -1231,6 +1300,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_title => 'Mapa de Nodos';
@override
String get map_lineOfSight => 'Línea de visión';
@override
String get map_losScreenTitle => 'Línea de visión';
@override
String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación';
@@ -1360,6 +1435,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_manageRepeater => 'Gestionar Repetidor';
@override
String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.';
@override
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
@override
String get map_removeLast => 'Eliminar último';
@override
String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.';
@override
String get mapCache_title => 'Caché de Mapa Offline';
@@ -1656,10 +1743,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
@override
String get repeater_neighbours => 'Vecinos';
String get repeater_neighbors => 'Vecinos';
@override
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.';
@override
String get repeater_settings => 'Configuración';
@@ -2358,7 +2445,7 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
String get neighbors_repeatersNeighbors => 'Repetidores Vecinos';
@override
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
@@ -2667,6 +2754,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get listFilter_all => 'Todas';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Añadir a favoritos';
@override
String get listFilter_removeFromFavorites => 'Eliminar de las favoritas';
@override
String get listFilter_users => 'Usuarios';
@@ -2698,6 +2794,149 @@ class AppLocalizationsEs extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Uno o más de los lúpulos carecen de una ubicación';
@override
String get pathTrace_clearTooltip => 'Borrar ruta';
@override
String get losSelectStartEnd =>
'Seleccione los nodos de inicio y fin para LOS.';
@override
String losRunFailed(String error) {
return 'Error en la comprobación de la línea de visión: $error';
}
@override
String get losClearAllPoints => 'Borrar todos los puntos';
@override
String get losRunToViewElevationProfile =>
'Ejecute LOS para ver el perfil de elevación';
@override
String get losMenuTitle => 'Menú LOS';
@override
String get losMenuSubtitle =>
'Toque nodos o mantenga presionado el mapa para puntos personalizados';
@override
String get losShowDisplayNodes => 'Mostrar nodos de visualización';
@override
String get losCustomPoints => 'Puntos personalizados';
@override
String losCustomPointLabel(int index) {
return 'Personalizado $index';
}
@override
String get losPointA => 'Punto A';
@override
String get losPointB => 'Punto B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Ejecutar LOS';
@override
String get losNoElevationData => 'Sin datos de elevación';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: comprobando...';
@override
String get losStatusNoData => 'LOS: sin datos';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido';
}
@override
String get losErrorElevationUnavailable =>
'Datos de elevación no disponibles para una o más muestras.';
@override
String get losErrorInvalidInput =>
'Datos de puntos/elevación no válidos para el cálculo de LOS.';
@override
String get losRenameCustomPoint =>
'Cambiar el nombre del punto personalizado';
@override
String get losPointName => 'Nombre del punto';
@override
String get losShowPanelTooltip => 'Mostrar panel LOS';
@override
String get losHidePanelTooltip => 'Ocultar panel LOS';
@override
String get losElevationAttribution =>
'Datos de elevación: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte radioeléctrico';
@override
String get losLegendLosBeam => 'Línea de visión';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frecuencia';
@override
String get losFrequencyInfoTooltip => 'Ver detalles del cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.';
}
@override
String get contacts_pathTrace => 'Rastreo de caminos';
@@ -2868,4 +3107,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exportación de datos de mapa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Repetidores cercanos';
@override
String get snrIndicator_lastSeen => 'Visto por última vez';
}
+299 -56
View File
@@ -143,6 +143,16 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get scanner_scan => 'Scanner';
@override
String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.';
@override
String get scanner_bluetoothOffMessage =>
'Veuillez activer le Bluetooth pour rechercher des appareils.';
@override
String get scanner_enableBluetooth => 'Activer le Bluetooth';
@override
String get device_quickSwitch => 'Basculement rapide';
@@ -314,6 +324,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_aboutDescription =>
'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nom';
@@ -338,15 +352,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_presets => 'Préréglages';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Fréquence (MHz)';
@@ -375,10 +380,15 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)';
@override
String get settings_longRange => 'Portée Longue';
String get settings_clientRepeat => 'Répétition hors réseau';
@override
String get settings_fastSpeed => 'Vitesse Rapide';
String get settings_clientRepeatSubtitle =>
'Permettez à cet appareil de répéter les paquets de données pour les autres.';
@override
String get settings_clientRepeatFreqWarning =>
'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.';
@override
String settings_error(String message) {
@@ -454,6 +464,14 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrainien';
@override
String get appSettings_enableMessageTracing =>
'Activer le traçage des messages';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Afficher les métadonnées détaillées de routage et de synchronisation des messages';
@override
String get appSettings_notifications => 'Notifications';
@@ -560,11 +578,11 @@ class AppLocalizationsFr extends AppLocalizations {
String get appSettings_mapDisplay => 'Affichage de la carte';
@override
String get appSettings_showRepeaters => 'Afficher les répétiteurs';
String get appSettings_showRepeaters => 'Afficher les répéteurs';
@override
String get appSettings_showRepeatersSubtitle =>
'Afficher les nœuds répétiteurs sur la carte';
'Afficher les nœuds répéteurs sur la carte';
@override
String get appSettings_showChatNodes => 'Afficher les nœuds de discussion';
@@ -616,6 +634,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne';
@override
String get appSettings_unitsTitle => 'Unités';
@override
String get appSettings_unitsMetric => 'Métrique (m/km)';
@override
String get appSettings_unitsImperial => 'Impérial (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Aucune zone sélectionnée';
@@ -654,7 +681,35 @@ class AppLocalizationsFr extends AppLocalizations {
'Les contacts apparaîtront lorsque les appareils font leur annonce.';
@override
String get contacts_searchContacts => 'Rechercher des contacts...';
String get contacts_unread => 'Non lu';
@override
String get contacts_searchContactsNoNumber => 'Rechercher des contacts...';
@override
String contacts_searchContacts(int number, String str) {
return 'Rechercher des contacts...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Rechercher $number$str Favoris...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Rechercher $number$str utilisateurs...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Rechercher $number$str Répéteurs...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Rechercher $number$str serveurs de salle...';
}
@override
String get contacts_noUnreadContacts => 'Aucun contact non lu';
@@ -671,13 +726,13 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get contacts_manageRepeater => 'Gérer le répétiteur';
String get contacts_manageRepeater => 'Gérer le répéteur';
@override
String get contacts_manageRoom => 'Gérer le Room Server';
@override
String get contacts_roomLogin => 'Connexion Salle';
String get contacts_roomLogin => 'Connexion Room Server';
@override
String get contacts_openChat => 'Ouverture du Chat';
@@ -779,6 +834,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channels_editChannel => 'Modifier le canal';
@override
String get channels_muteChannel => 'Désactiver les notifications du canal';
@override
String get channels_unmuteChannel => 'Réactiver les notifications du canal';
@override
String get channels_deleteChannel => 'Supprimer le canal';
@@ -787,6 +848,11 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Supprimer $name? Cela ne peut pas être annulé.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Échec de la suppression de la chaîne \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Le canal \"$name\" a été supprimé';
@@ -1076,6 +1142,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestion des chemins';
@override
String get chat_ShowAllPaths => 'Afficher tous les chemins';
@override
String get chat_routingMode => 'Mode de routage';
@@ -1094,18 +1163,18 @@ class AppLocalizationsFr extends AppLocalizations {
'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.';
@override
String get chat_hopSingular => 'Sautez';
String get chat_hopSingular => 'saut';
@override
String get chat_hopPlural => 'sautez';
String get chat_hopPlural => 'sauts';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
other: 'sauts',
one: 'saut',
);
return '$count $_temp0';
}
@@ -1237,6 +1306,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_title => 'Carte des nœuds';
@override
String get map_lineOfSight => 'Ligne de vue';
@override
String get map_losScreenTitle => 'Ligne de vue';
@override
String get map_noNodesWithLocation =>
'Aucun nœud avec des données de localisation';
@@ -1259,7 +1334,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_chat => 'Chat';
@override
String get map_repeater => 'Répétiteur';
String get map_repeater => 'Répéteur';
@override
String get map_room => 'Salle';
@@ -1365,7 +1440,20 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_joinRoom => 'Rejoindre la salle';
@override
String get map_manageRepeater => 'Gérer le répétiteur';
String get map_manageRepeater => 'Gérer le répéteur';
@override
String get map_tapToAdd =>
'Appuyez sur les nœuds pour les ajouter au chemin.';
@override
String get map_runTrace => 'Exécuter la traçage de chemin';
@override
String get map_removeLast => 'Supprimer le dernier';
@override
String get map_pathTraceCancelled => 'Traçage de chemin annulé';
@override
String get mapCache_title => 'Cache de Carte Hors Ligne';
@@ -1509,10 +1597,10 @@ class AppLocalizationsFr extends AppLocalizations {
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
@override
String get login_repeaterLogin => 'Connexion au répétiteur';
String get login_repeaterLogin => 'Connexion au répéteur';
@override
String get login_roomLogin => 'Connexion Salle';
String get login_roomLogin => 'Connexion Room Server';
@override
String get login_password => 'Mot de passe';
@@ -1529,7 +1617,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get login_repeaterDescription =>
'Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l\'état.';
'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.';
@override
String get login_roomDescription =>
@@ -1634,10 +1722,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get path_setPath => 'Définir le chemin';
@override
String get repeater_management => 'Gestion des répétiteurs';
String get repeater_management => 'Gestion des répéteurs';
@override
String get room_management => 'Administración del Servidor de Habitación';
String get room_management => 'Administrattion Room Server';
@override
String get repeater_managementTools => 'Outils de Gestion';
@@ -1647,7 +1735,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_statusSubtitle =>
'Afficher l\'état, les statistiques et les voisins du répétiteur';
'Afficher l\'état, les statistiques et les voisins du répéteur';
@override
String get repeater_telemetry => 'Télémetrie';
@@ -1660,24 +1748,23 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_cli => 'CLI';
@override
String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur';
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
@override
String get repeater_neighbours => 'Voisins';
String get repeater_neighbors => 'Voisins';
@override
String get repeater_neighboursSubtitle =>
'Afficher les voisins de saut nuls.';
String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.';
@override
String get repeater_settings => 'Paramètres';
@override
String get repeater_settingsSubtitle =>
'Configurer les paramètres du répétiteur';
'Configurer les paramètres du répéteur';
@override
String get repeater_statusTitle => 'État du répétiteur';
String get repeater_statusTitle => 'État du répéteur';
@override
String get repeater_routingMode => 'Mode de routage';
@@ -1783,16 +1870,16 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get repeater_settingsTitle => 'Paramètres du répétiteur';
String get repeater_settingsTitle => 'Paramètres du répéteur';
@override
String get repeater_basicSettings => 'Paramètres de base';
@override
String get repeater_repeaterName => 'Nom du répétiteur';
String get repeater_repeaterName => 'Nom du répéteur';
@override
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répétiteur';
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur';
@override
String get repeater_adminPassword => 'Mot de passe Administrateur';
@@ -1856,7 +1943,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_packetForwardingSubtitle =>
'Activer le répétiteur pour transmettre des paquets';
'Activer le répéteur pour transmettre des paquets';
@override
String get repeater_guestAccess => 'Accès Invité';
@@ -1905,11 +1992,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_rebootRepeaterSubtitle =>
'Réinitialiser l\'appareil répétiteur';
'Réinitialiser l\'appareil répéteur';
@override
String get repeater_rebootRepeaterConfirm =>
'Êtes-vous sûr de vouloir redémarrer ce répétiteur ?';
'Êtes-vous sûr de vouloir redémarrer ce répéteur ?';
@override
String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité';
@@ -1920,18 +2007,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_regenerateIdentityKeyConfirm =>
'Cela générera une nouvelle identité pour le répétiteur. Continuer ?';
'Cela générera une nouvelle identité pour le répéteur. Continuer ?';
@override
String get repeater_eraseFileSystem => 'Supprimer le système de fichiers';
@override
String get repeater_eraseFileSystemSubtitle =>
'Formater le système de fichiers du répétiteur';
'Formater le système de fichiers du répéteur';
@override
String get repeater_eraseFileSystemConfirm =>
'AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !';
'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !';
@override
String get repeater_eraseSerialOnly =>
@@ -1999,7 +2086,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get repeater_cliTitle => 'Répétiteur CLI';
String get repeater_cliTitle => 'Répéteur CLI';
@override
String get repeater_debugNextCommand => 'Déboguer Prochaine Commande';
@@ -2091,7 +2178,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpSetRepeat =>
'Active ou désactive le rôle du répétiteur pour ce nœud.';
'Active ou désactive le rôle du répéteur pour ce nœud.';
@override
String get repeater_cliHelpSetAllowReadOnly =>
@@ -2115,7 +2202,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpSetAdvertInterval =>
'Définit l\'intervalle du minuteur pour envoyer un paquet d\'annonce local (sans relais). Définir sur 0 pour désactiver.';
'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.';
@override
String get repeater_cliHelpSetFloodAdvertInterval =>
@@ -2201,7 +2288,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpNeighbors =>
'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
@override
String get repeater_cliHelpNeighborRemove =>
@@ -2289,12 +2376,11 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_logging => 'Journalisation';
@override
String get repeater_neighborsRepeaterOnly =>
'Voisins (Uniquement répétiteur)';
String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)';
@override
String get repeater_regionManagementRepeaterOnly =>
'Gestion des régions (uniquement pour le répétiteur)';
'Gestion des régions (uniquement pour le répéteur)';
@override
String get repeater_regionNote =>
@@ -2373,7 +2459,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
String get neighbors_repeatersNeighbors => 'Répéteurs Voisins';
@override
String get neighbors_noData =>
@@ -2399,7 +2485,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channelPath_otherObservedPaths => 'Autres chemins observés';
@override
String get channelPath_repeaterHops => 'Sauts du répétiteur';
String get channelPath_repeaterHops => 'Sauts du répéteur';
@override
String get channelPath_noHopDetails =>
@@ -2467,7 +2553,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channelPath_noRepeaterLocations =>
'Aucune position de répétiteur disponible pour ce chemin.';
'Aucune position de répéteur disponible pour ce chemin.';
@override
String channelPath_primaryPath(int index) {
@@ -2684,6 +2770,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get listFilter_all => 'Tout';
@override
String get listFilter_favorites => 'Préférences';
@override
String get listFilter_addToFavorites => 'Ajouter à mes favoris';
@override
String get listFilter_removeFromFavorites => 'Supprimer des favoris';
@override
String get listFilter_users => 'Utilisateurs';
@@ -2713,7 +2808,149 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get pathTrace_someHopsNoLocation =>
'Une ou plusieurs des houblons manquent d\'une localisation !';
'Un ou plusieurs des sauts manquent d\'une localisation !';
@override
String get pathTrace_clearTooltip => 'Effacer le chemin';
@override
String get losSelectStartEnd =>
'Sélectionnez les nœuds de début et de fin pour LOS.';
@override
String losRunFailed(String error) {
return 'Échec de la vérification de la ligne de vue : $error';
}
@override
String get losClearAllPoints => 'Effacer tous les points';
@override
String get losRunToViewElevationProfile =>
'Exécutez LOS pour afficher le profil d\'altitude';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés';
@override
String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage';
@override
String get losCustomPoints => 'Points personnalisés';
@override
String losCustomPointLabel(int index) {
return 'Personnalisé $index';
}
@override
String get losPointA => 'Point A';
@override
String get losPointB => 'Point B';
@override
String losAntennaA(String value, String unit) {
return 'Antenne A : $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenne B : $value $unit';
}
@override
String get losRun => 'Exécuter la LOS';
@override
String get losNoElevationData => 'Aucune donnée d\'altitude';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, LOS clair, clairance minimale $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqué par $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS : vérification...';
@override
String get losStatusNoData => 'LOS : aucune donnée';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu';
}
@override
String get losErrorElevationUnavailable =>
'Données d\'altitude indisponibles pour un ou plusieurs échantillons.';
@override
String get losErrorInvalidInput =>
'Données de points/d\'altitude non valides pour le calcul de la LOS.';
@override
String get losRenameCustomPoint => 'Renommer le point personnalisé';
@override
String get losPointName => 'Nom du point';
@override
String get losShowPanelTooltip => 'Afficher le panneau LOS';
@override
String get losHidePanelTooltip => 'Masquer le panneau LOS';
@override
String get losElevationAttribution =>
'Données daltitude : Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizon radio';
@override
String get losLegendLosBeam => 'Ligne de visée';
@override
String get losLegendTerrain => 'Terrain';
@override
String get losFrequencyLabel => 'Fréquence';
@override
String get losFrequencyInfoTooltip => 'Voir les détails du calcul';
@override
String get losFrequencyDialogTitle => 'Calcul de lhorizon radio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.';
}
@override
String get contacts_pathTrace => 'Traçage de chemin';
@@ -2756,10 +2993,10 @@ class AppLocalizationsFr extends AppLocalizations {
'Échec de l\'importation du contact.';
@override
String get contacts_zeroHopAdvert => 'Annonce Zero Hop';
String get contacts_zeroHopAdvert => 'Annonce Zero saut';
@override
String get contacts_floodAdvert => 'Annonce de crue';
String get contacts_floodAdvert => 'Annonce à tout le réseau';
@override
String get contacts_copyAdvertToClipboard =>
@@ -2892,4 +3129,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exporter les données de carte GPX';
@override
String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité';
@override
String get snrIndicator_lastSeen => 'Dernière fois vu';
}
+259 -15
View File
@@ -143,6 +143,16 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_scan => 'Scansiona';
@override
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
@override
String get scanner_bluetoothOffMessage =>
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
@override
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
@override
String get device_quickSwitch => 'Passa velocemente';
@@ -312,6 +322,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_aboutDescription =>
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nome';
@@ -336,15 +350,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequenza (MHz)';
@@ -373,10 +378,15 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)';
@override
String get settings_longRange => 'Lungo Raggio';
String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"';
@override
String get settings_fastSpeed => 'Velocità Rapida';
String get settings_clientRepeatSubtitle =>
'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.';
@override
String get settings_clientRepeatFreqWarning =>
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
@override
String settings_error(String message) {
@@ -452,6 +462,14 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ucraino';
@override
String get appSettings_enableMessageTracing =>
'Abilita tracciamento messaggi';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Mostra metadati dettagliati su instradamento e tempi per i messaggi';
@override
String get appSettings_notifications => 'Notifiche';
@@ -613,6 +631,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
@override
String get appSettings_unitsTitle => 'Unità';
@override
String get appSettings_unitsMetric => 'Metrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperiale (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
@@ -650,7 +677,35 @@ class AppLocalizationsIt extends AppLocalizations {
'I contatti appariranno quando i dispositivi pubblicizzano.';
@override
String get contacts_searchContacts => 'Cerca contatti...';
String get contacts_unread => 'Non letti';
@override
String get contacts_searchContactsNoNumber => 'Cerca Contatti...';
@override
String contacts_searchContacts(int number, String str) {
return 'Cerca contatti...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Cerca $number$str Preferiti...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Cerca $number$str Utenti...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Cerca $number$str Ripetitori...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Cerca $number$str server Room...';
}
@override
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
@@ -775,6 +830,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_editChannel => 'Modifica canale';
@override
String get channels_muteChannel => 'Silenzia canale';
@override
String get channels_unmuteChannel => 'Attiva notifiche canale';
@override
String get channels_deleteChannel => 'Elimina canale';
@@ -783,6 +844,11 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Eliminare \"$name\"? Non può essere annullato.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Impossibile eliminare il canale \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canale \"$name\" eliminato';
@@ -1071,6 +1137,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestione Percorsi';
@override
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
@override
String get chat_routingMode => 'Modalità di routing';
@@ -1230,6 +1299,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_title => 'Mappa Nodi';
@override
String get map_lineOfSight => 'Linea di vista';
@override
String get map_losScreenTitle => 'Linea di vista';
@override
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
@@ -1359,6 +1434,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_manageRepeater => 'Gestisci Ripetitore';
@override
String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.';
@override
String get map_runTrace => 'Esegui Path Trace';
@override
String get map_removeLast => 'Rimuovi ultimo';
@override
String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.';
@override
String get mapCache_title => 'Cache Mappa Offline';
@@ -1654,10 +1741,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
@override
String get repeater_neighbours => 'Vicini';
String get repeater_neighbors => 'Vicini';
@override
String get repeater_neighboursSubtitle =>
String get repeater_neighborsSubtitle =>
'Visualizza vicini di salto pari a zero.';
@override
@@ -2358,7 +2445,7 @@ class AppLocalizationsIt extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
@override
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
@@ -2667,6 +2754,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get listFilter_all => 'Tutti';
@override
String get listFilter_favorites => 'Preferiti';
@override
String get listFilter_addToFavorites => 'Aggiungi ai preferiti';
@override
String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti';
@override
String get listFilter_users => 'Utenti';
@@ -2699,6 +2795,148 @@ class AppLocalizationsIt extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Uno o più dei luppoli mancano di una posizione!';
@override
String get pathTrace_clearTooltip => 'Pulisci percorso';
@override
String get losSelectStartEnd =>
'Seleziona i nodi iniziali e finali per la LOS.';
@override
String losRunFailed(String error) {
return 'Controllo della linea di vista fallito: $error';
}
@override
String get losClearAllPoints => 'Cancella tutti i punti';
@override
String get losRunToViewElevationProfile =>
'Eseguire LOS per visualizzare il profilo altimetrico';
@override
String get losMenuTitle => 'Menù LOS';
@override
String get losMenuSubtitle =>
'Tocca i nodi o premi a lungo la mappa per punti personalizzati';
@override
String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione';
@override
String get losCustomPoints => 'Punti personalizzati';
@override
String losCustomPointLabel(int index) {
return 'Personalizzato $index';
}
@override
String get losPointA => 'Punto A';
@override
String get losPointB => 'Punto B';
@override
String losAntennaA(String value, String unit) {
return 'Antenna A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenna B: $value $unit';
}
@override
String get losRun => 'Esegui LOS';
@override
String get losNoElevationData => 'Nessun dato di elevazione';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloccato da $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: controllo...';
@override
String get losStatusNoData => 'LOS: nessun dato';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto';
}
@override
String get losErrorElevationUnavailable =>
'Dati di elevazione non disponibili per uno o più campioni.';
@override
String get losErrorInvalidInput =>
'Dati punti/elevazione non validi per il calcolo della LOS.';
@override
String get losRenameCustomPoint => 'Rinomina punto personalizzato';
@override
String get losPointName => 'Nome del punto';
@override
String get losShowPanelTooltip => 'Mostra il pannello LOS';
@override
String get losHidePanelTooltip => 'Nascondi il pannello LOS';
@override
String get losElevationAttribution =>
'Dati di elevazione: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Orizzonte radio';
@override
String get losLegendLosBeam => 'Linea di vista';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequenza';
@override
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
@override
String get losFrequencyDialogTitle => 'Calcolo dellorizzonte radio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.';
}
@override
String get contacts_pathTrace => 'Traccia Percorso';
@@ -2872,4 +3110,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open esportazione dati mappa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
@override
String get snrIndicator_lastSeen => 'Ultimo accesso';
}
+259 -15
View File
@@ -142,6 +142,16 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get scanner_scan => 'Scan';
@override
String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld';
@override
String get scanner_bluetoothOffMessage =>
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
@override
String get scanner_enableBluetooth => 'Activeer Bluetooth';
@override
String get device_quickSwitch => 'Snelle overschakeling';
@@ -310,6 +320,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_aboutDescription =>
'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Naam';
@@ -334,15 +348,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequentie (MHz)';
@@ -371,10 +376,15 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
@override
String get settings_longRange => 'Lange Afstand';
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
@override
String get settings_fastSpeed => 'Hoge Snelheid';
String get settings_clientRepeatSubtitle =>
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
@override
String get settings_clientRepeatFreqWarning =>
'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.';
@override
String settings_error(String message) {
@@ -450,6 +460,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_languageUk => 'Oekraïens';
@override
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Gedetailleerde routerings- en timing-metadata voor berichten weergeven';
@override
String get appSettings_notifications => 'Notificaties';
@@ -611,6 +628,15 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
@override
String get appSettings_unitsTitle => 'Eenheden';
@override
String get appSettings_unitsMetric => 'Metrisch (m / km)';
@override
String get appSettings_unitsImperial => 'Imperiaal (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Geen gebied geselecteerd';
@@ -648,7 +674,35 @@ class AppLocalizationsNl extends AppLocalizations {
'Contacten verschijnen wanneer apparaten zich aanbieden.';
@override
String get contacts_searchContacts => 'Zoek contacten...';
String get contacts_unread => 'Ongelezen';
@override
String get contacts_searchContactsNoNumber => 'Zoek contacten...';
@override
String contacts_searchContacts(int number, String str) {
return 'Zoek contacten...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Zoek $number$str favorieten...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Zoek $number$str gebruikers...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Zoek $number$str Repeaters...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Zoek $number$str Room servers...';
}
@override
String get contacts_noUnreadContacts => 'Geen ongelezen contacten';
@@ -773,6 +827,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channels_editChannel => 'Kanaal bewerken';
@override
String get channels_muteChannel => 'Kanaal dempen';
@override
String get channels_unmuteChannel => 'Kanaal dempen opheffen';
@override
String get channels_deleteChannel => 'Kanaal verwijderen';
@@ -781,6 +841,11 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kan kanaal $name niet verwijderen';
}
@override
String channels_channelDeleted(String name) {
return 'Kanaal \"$name\" verwijderd';
@@ -1068,6 +1133,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_pathManagement => 'Beheer van Paden';
@override
String get chat_ShowAllPaths => 'Toon alle paden';
@override
String get chat_routingMode => 'Routeerwijze';
@@ -1226,6 +1294,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_title => 'Node Map';
@override
String get map_lineOfSight => 'Zichtlijn';
@override
String get map_losScreenTitle => 'Zichtlijn';
@override
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
@@ -1355,6 +1429,19 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_manageRepeater => 'Beheer Repeater';
@override
String get map_tapToAdd =>
'Tik op knooppunten om ze toe te voegen aan het pad';
@override
String get map_runTrace => 'Padeshulp traceren';
@override
String get map_removeLast => 'Verwijder Laatste';
@override
String get map_pathTraceCancelled => 'Pad traceren geannuleerd';
@override
String get mapCache_title => 'Offline Kaarten Cache';
@@ -1649,10 +1736,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
@override
String get repeater_neighbours => 'Buren';
String get repeater_neighbors => 'Buren';
@override
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
@override
String get repeater_settings => 'Instellingen';
@@ -2348,7 +2435,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
@override
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
@@ -2658,6 +2745,15 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get listFilter_all => 'Alles';
@override
String get listFilter_favorites => 'Favorieten';
@override
String get listFilter_addToFavorites => 'Toevoegen aan favorieten';
@override
String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten';
@override
String get listFilter_users => 'Gebruikers';
@@ -2689,6 +2785,148 @@ class AppLocalizationsNl extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Een of meer van de hops ontbreken een locatie!';
@override
String get pathTrace_clearTooltip => 'Weg wissen';
@override
String get losSelectStartEnd =>
'Selecteer begin- en eindknooppunten voor LOS.';
@override
String losRunFailed(String error) {
return 'Zichtlijncontrole mislukt: $error';
}
@override
String get losClearAllPoints => 'Wis alle punten';
@override
String get losRunToViewElevationProfile =>
'Voer LOS uit om het hoogteprofiel te bekijken';
@override
String get losMenuTitle => 'LOS-menu';
@override
String get losMenuSubtitle =>
'Tik op knooppunten of druk lang op de kaart voor aangepaste punten';
@override
String get losShowDisplayNodes => 'Toon weergaveknooppunten';
@override
String get losCustomPoints => 'Aangepaste punten';
@override
String losCustomPointLabel(int index) {
return 'Aangepast $index';
}
@override
String get losPointA => 'Punt A';
@override
String get losPointB => 'Punt B';
@override
String losAntennaA(String value, String unit) {
return 'Antenne A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenne B: $value $unit';
}
@override
String get losRun => 'Voer LOS uit';
@override
String get losNoElevationData => 'Geen hoogtegegevens';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, vrije LOS, min. vrije ruimte $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, geblokkeerd door $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: controleren...';
@override
String get losStatusNoData => 'LOS: geen gegevens';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total gewist, $blocked geblokkeerd, $unknown onbekend';
}
@override
String get losErrorElevationUnavailable =>
'Hoogtegegevens niet beschikbaar voor een of meer monsters.';
@override
String get losErrorInvalidInput =>
'Ongeldige punten/hoogtegegevens voor LOS-berekening.';
@override
String get losRenameCustomPoint => 'Hernoem aangepast punt';
@override
String get losPointName => 'Puntnaam';
@override
String get losShowPanelTooltip => 'Toon LOS-paneel';
@override
String get losHidePanelTooltip => 'LOS-paneel verbergen';
@override
String get losElevationAttribution =>
'Hoogtegegevens: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radiohorizon';
@override
String get losLegendLosBeam => 'Zichtlijn';
@override
String get losLegendTerrain => 'Terrein';
@override
String get losFrequencyLabel => 'Frequentie';
@override
String get losFrequencyInfoTooltip => 'Bekijk details van de berekening';
@override
String get losFrequencyDialogTitle => 'Berekening van de radiohorizon';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.';
}
@override
String get contacts_pathTrace => 'Pad Traceren';
@@ -2859,4 +3097,10 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open GPX kaartgegevens exporteren';
@override
String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden';
@override
String get snrIndicator_lastSeen => 'Laatst gezien';
}
+257 -15
View File
@@ -143,6 +143,16 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get scanner_scan => 'Przeskanuj';
@override
String get scanner_bluetoothOff => 'Bluetooth jest wyłączony';
@override
String get scanner_bluetoothOffMessage =>
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
@override
String get scanner_enableBluetooth => 'Włącz Bluetooth';
@override
String get device_quickSwitch => 'Szybka zmiana';
@@ -313,6 +323,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_aboutDescription =>
'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Imię';
@@ -337,15 +351,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Częstotliwość (MHz)';
@@ -375,10 +380,15 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)';
@override
String get settings_longRange => 'Długi zasięg';
String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci';
@override
String get settings_fastSpeed => 'Szybka prędkość';
String get settings_clientRepeatSubtitle =>
'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.';
@override
String get settings_clientRepeatFreqWarning =>
'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.';
@override
String settings_error(String message) {
@@ -454,6 +464,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukraińska';
@override
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości';
@override
String get appSettings_notifications => 'Powiadomienia';
@@ -615,6 +632,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Bufor Map Offline';
@override
String get appSettings_unitsTitle => 'Jednostki';
@override
String get appSettings_unitsMetric => 'Metryczne (m / km)';
@override
String get appSettings_unitsImperial => 'Imperialne (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
@@ -652,7 +678,35 @@ class AppLocalizationsPl extends AppLocalizations {
'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
@override
String get contacts_searchContacts => 'Wyszukaj kontakty...';
String get contacts_unread => 'Nieprzeczytane';
@override
String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...';
@override
String contacts_searchContacts(int number, String str) {
return 'Wyszukaj kontakty...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Wyszukaj $number$str ulubione...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Wyszukaj $number$str Użytkowników...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Wyszukaj $number$str powtórników...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Wyszukaj $number$str serwerów Room...';
}
@override
String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów';
@@ -778,6 +832,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channels_editChannel => 'Edytuj kanał';
@override
String get channels_muteChannel => 'Wycisz kanał';
@override
String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału';
@override
String get channels_deleteChannel => 'Usuń kanał';
@@ -786,6 +846,11 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Usuń \"$name\"? Nie można tego cofnąć.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Nie udało się usunąć kanału \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Kanał \"$name\" usunięto';
@@ -1073,6 +1138,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_pathManagement => 'Zarządzanie ścieżkami';
@override
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
@override
String get chat_routingMode => 'Tryb routingu';
@@ -1232,6 +1300,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_title => 'Mapa węzłów';
@override
String get map_lineOfSight => 'Linia wzroku';
@override
String get map_losScreenTitle => 'Linia wzroku';
@override
String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi';
@@ -1361,6 +1435,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.';
@override
String get map_runTrace => 'Uruchom ślad ścieżki';
@override
String get map_removeLast => 'Usuń ostatni';
@override
String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.';
@override
String get mapCache_title => 'Bufor Map Offline';
@@ -1658,10 +1744,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
@override
String get repeater_neighbours => 'Sąsiedzi';
String get repeater_neighbors => 'Sąsiedzi';
@override
String get repeater_neighboursSubtitle =>
String get repeater_neighborsSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
@override
@@ -2357,7 +2443,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
@override
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
@@ -2666,6 +2752,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get listFilter_all => 'Wszystko';
@override
String get listFilter_favorites => 'Ulubione';
@override
String get listFilter_addToFavorites => 'Dodaj do ulubionych';
@override
String get listFilter_removeFromFavorites => 'Usuń z ulubionych';
@override
String get listFilter_users => 'Użytkownicy';
@@ -2697,6 +2792,147 @@ class AppLocalizationsPl extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Jeden lub więcej z chmieli nie ma określonej lokalizacji!';
@override
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
@override
String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.';
@override
String losRunFailed(String error) {
return 'Sprawdzenie pola widzenia nie powiodło się: $error';
}
@override
String get losClearAllPoints => 'Wyczyść wszystkie punkty';
@override
String get losRunToViewElevationProfile =>
'Uruchom LOS, aby wyświetlić profil wysokości';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty';
@override
String get losShowDisplayNodes => 'Pokaż węzły wyświetlające';
@override
String get losCustomPoints => 'Punkty niestandardowe';
@override
String losCustomPointLabel(int index) {
return 'Niestandardowe $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Uruchom LOS-a';
@override
String get losNoElevationData => 'Brak danych o wysokości';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: sprawdzam...';
@override
String get losStatusNoData => 'LOS: brak danych';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane';
}
@override
String get losErrorElevationUnavailable =>
'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.';
@override
String get losErrorInvalidInput =>
'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.';
@override
String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego';
@override
String get losPointName => 'Nazwa punktu';
@override
String get losShowPanelTooltip => 'Pokaż panel LOS';
@override
String get losHidePanelTooltip => 'Ukryj panel LOS';
@override
String get losElevationAttribution =>
'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horyzont radiowy';
@override
String get losLegendLosBeam => 'Linia widoczności';
@override
String get losLegendTerrain => 'Teren';
@override
String get losFrequencyLabel => 'Częstotliwość';
@override
String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia';
@override
String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.';
}
@override
String get contacts_pathTrace => 'Śledzenie Ścieżek';
@@ -2874,4 +3110,10 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'Eksport danych mapy GPX meshcore-open';
@override
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
@override
String get snrIndicator_lastSeen => 'Ostatnio widziany';
}
+258 -16
View File
@@ -143,6 +143,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_scan => 'Digitalizar';
@override
String get scanner_bluetoothOff => 'Bluetooth está desativado';
@override
String get scanner_bluetoothOffMessage =>
'Por favor, ative o Bluetooth para escanear por dispositivos.';
@override
String get scanner_enableBluetooth => 'Ative o Bluetooth';
@override
String get device_quickSwitch => 'Mudar rapidamente';
@@ -314,6 +324,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_aboutDescription =>
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dados de elevação LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nome';
@@ -338,15 +352,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequência (MHz)';
@@ -375,10 +380,15 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)';
@override
String get settings_longRange => 'Alcance Longo';
String get settings_clientRepeat => 'Repetição sem rede';
@override
String get settings_fastSpeed => 'Velocidade Rápida';
String get settings_clientRepeatSubtitle =>
'Permita que este dispositivo repita pacotes de rede para outros dispositivos.';
@override
String get settings_clientRepeatFreqWarning =>
'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.';
@override
String settings_error(String message) {
@@ -454,6 +464,14 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ucraniano';
@override
String get appSettings_enableMessageTracing =>
'Ativar rastreamento de mensagens';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Mostrar metadados detalhados de roteamento e tempo para as mensagens';
@override
String get appSettings_notifications => 'Notificações';
@@ -614,6 +632,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
@override
String get appSettings_unitsTitle => 'Unidades';
@override
String get appSettings_unitsMetric => 'Métrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
@override
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
@@ -652,7 +679,35 @@ class AppLocalizationsPt extends AppLocalizations {
'Os contatos serão exibidos quando os dispositivos anunciarem.';
@override
String get contacts_searchContacts => 'Pesquisar contatos...';
String get contacts_unread => 'Não lido';
@override
String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...';
@override
String contacts_searchContacts(int number, String str) {
return 'Pesquisar contatos...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Pesquisar $number$str Favoritos...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Pesquisar $number$str Usuários...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Pesquisar $number$str Repetidores...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Pesquisar $number$str servidores de sala...';
}
@override
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
@@ -778,6 +833,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channels_editChannel => 'Editar canal';
@override
String get channels_muteChannel => 'Silenciar canal';
@override
String get channels_unmuteChannel => 'Ativar canal';
@override
String get channels_deleteChannel => 'Excluir canal';
@@ -786,6 +847,11 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Excluir \"$name\"? Não pode ser desfeito.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Falha ao excluir o canal \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canal \"$name\" excluído';
@@ -1073,6 +1139,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get chat_pathManagement => 'Gerenciamento de Caminhos';
@override
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
@override
String get chat_routingMode => 'Modo de roteamento';
@@ -1231,6 +1300,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_title => 'Mapa de Nós';
@override
String get map_lineOfSight => 'Linha de visão';
@override
String get map_losScreenTitle => 'Linha de visão';
@override
String get map_noNodesWithLocation =>
'Não existem nós com dados de localização.';
@@ -1361,6 +1436,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_manageRepeater => 'Gerenciar Repetidor';
@override
String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.';
@override
String get map_runTrace => 'Executar Traçado de Caminho';
@override
String get map_removeLast => 'Remover Último';
@override
String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.';
@override
String get mapCache_title => 'Cache de Mapa Offline';
@@ -1656,11 +1743,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
@override
String get repeater_neighbours => 'Vizinhos';
String get repeater_neighbors => 'Vizinhos';
@override
String get repeater_neighboursSubtitle =>
'Visualizar vizinhos de salto zero.';
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
@override
String get repeater_settings => 'Configurações';
@@ -2359,7 +2445,7 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
@override
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
@@ -2669,6 +2755,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get listFilter_all => 'Tudo';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Adicionar aos favoritos';
@override
String get listFilter_removeFromFavorites => 'Remover da lista de favoritos';
@override
String get listFilter_users => 'Usuários';
@@ -2700,6 +2795,147 @@ class AppLocalizationsPt extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Um ou mais dos lúpulos estão sem localização!';
@override
String get pathTrace_clearTooltip => 'Limpar caminho';
@override
String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.';
@override
String losRunFailed(String error) {
return 'Falha na verificação da linha de visão: $error';
}
@override
String get losClearAllPoints => 'Limpe todos os pontos';
@override
String get losRunToViewElevationProfile =>
'Execute o LOS para visualizar o perfil de elevação';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados';
@override
String get losShowDisplayNodes => 'Mostrar nós de exibição';
@override
String get losCustomPoints => 'Pontos personalizados';
@override
String losCustomPointLabel(int index) {
return '$index personalizado';
}
@override
String get losPointA => 'Ponto A';
@override
String get losPointB => 'Ponto B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Executar LOS';
@override
String get losNoElevationData => 'Sem dados de elevação';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: verificando...';
@override
String get losStatusNoData => 'LOS: sem dados';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido';
}
@override
String get losErrorElevationUnavailable =>
'Dados de elevação indisponíveis para uma ou mais amostras.';
@override
String get losErrorInvalidInput =>
'Dados de pontos/elevação inválidos para cálculo de LOS.';
@override
String get losRenameCustomPoint => 'Renomear ponto personalizado';
@override
String get losPointName => 'Nome do ponto';
@override
String get losShowPanelTooltip => 'Mostrar painel LOS';
@override
String get losHidePanelTooltip => 'Ocultar painel LOS';
@override
String get losElevationAttribution =>
'Dados de elevação: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte de rádio';
@override
String get losLegendLosBeam => 'Linha de visada';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequência';
@override
String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.';
}
@override
String get contacts_pathTrace => 'Traçado de Caminho';
@@ -2869,4 +3105,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exportação de dados de mapa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Repetidores Próximos';
@override
String get snrIndicator_lastSeen => 'Visto pela última vez';
}
+258 -15
View File
@@ -142,6 +142,16 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get scanner_scan => 'Сканирование';
@override
String get scanner_bluetoothOff => 'Bluetooth выключен';
@override
String get scanner_bluetoothOffMessage =>
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
@override
String get scanner_enableBluetooth => 'Включите Bluetooth';
@override
String get device_quickSwitch => 'Быстрое переключение';
@@ -311,6 +321,10 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_aboutDescription =>
'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Данные о высоте LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Имя';
@@ -335,15 +349,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_presets => 'Пресеты';
@override
String get settings_preset915Mhz => '915 МГц';
@override
String get settings_preset868Mhz => '868 МГц';
@override
String get settings_preset433Mhz => '433 МГц';
@override
String get settings_frequency => 'Частота (МГц)';
@@ -373,10 +378,15 @@ class AppLocalizationsRu extends AppLocalizations {
'Недопустимая мощность передачи (0–22 дБм)';
@override
String get settings_longRange => 'Дальний радиус';
String get settings_clientRepeat => 'Повторение \"вне сети\"';
@override
String get settings_fastSpeed => 'Высокая скорость';
String get settings_clientRepeatSubtitle =>
'Позвольте этому устройству повторять пакеты данных для других устройств.';
@override
String get settings_clientRepeatFreqWarning =>
'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.';
@override
String settings_error(String message) {
@@ -452,6 +462,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_enableMessageTracing =>
'Включить трассировку сообщений';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Показывать подробные метаданные о маршрутизации и времени для сообщений';
@override
String get appSettings_notifications => 'Уведомления';
@@ -614,6 +632,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
@override
String get appSettings_unitsTitle => 'Единицы';
@override
String get appSettings_unitsMetric => 'Метрическая (м/км)';
@override
String get appSettings_unitsImperial => 'Имперская (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Область не выбрана';
@@ -651,7 +678,35 @@ class AppLocalizationsRu extends AppLocalizations {
'Контакты появятся, когда устройства начнут рассылать оповещения';
@override
String get contacts_searchContacts => 'Поиск контактов...';
String get contacts_unread => 'Непрочитанное';
@override
String get contacts_searchContactsNoNumber => 'Поиск контактов...';
@override
String contacts_searchContacts(int number, String str) {
return 'Поиск контактов...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Поиск $number$str избранного...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Поиск $number$str пользователей...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Поиск $number$str ретрансляторов...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Поиск $number$str серверов комнат...';
}
@override
String get contacts_noUnreadContacts => 'Нет непрочитанных контактов';
@@ -776,6 +831,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get channels_editChannel => 'Изменить канал';
@override
String get channels_muteChannel => 'Отключить уведомления канала';
@override
String get channels_unmuteChannel => 'Включить уведомления канала';
@override
String get channels_deleteChannel => 'Удалить канал';
@@ -784,6 +845,11 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Удалить \"$name\"? Это действие нельзя отменить.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Не удалось удалить канал $name.';
}
@override
String channels_channelDeleted(String name) {
return 'Канал \"$name\" удалён';
@@ -1071,6 +1137,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_pathManagement => 'Управление маршрутами';
@override
String get chat_ShowAllPaths => 'Показать все пути';
@override
String get chat_routingMode => 'Режим маршрутизации';
@@ -1233,6 +1302,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_title => 'Карта нод';
@override
String get map_lineOfSight => 'Линия видимости';
@override
String get map_losScreenTitle => 'Линия видимости';
@override
String get map_noNodesWithLocation => 'Нет нод с данными о местоположении';
@@ -1362,6 +1437,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_manageRepeater => 'Управление репитером';
@override
String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.';
@override
String get map_runTrace => 'Запустить трассировку пути';
@override
String get map_removeLast => 'Удалить последний';
@override
String get map_pathTraceCancelled => 'Отмена трассировки пути';
@override
String get mapCache_title => 'Кэш офлайн-карты';
@@ -1658,10 +1745,10 @@ class AppLocalizationsRu extends AppLocalizations {
String get repeater_cliSubtitle => 'Отправка команд репитеру';
@override
String get repeater_neighbours => 'Соседи';
String get repeater_neighbors => 'Соседи';
@override
String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.';
String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.';
@override
String get repeater_settings => 'Настройки';
@@ -2361,7 +2448,7 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Соседи репитеров';
String get neighbors_repeatersNeighbors => 'Соседи репитеров';
@override
String get neighbors_noData => 'Данные о соседях недоступны.';
@@ -2671,6 +2758,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get listFilter_all => 'Все';
@override
String get listFilter_favorites => 'Избранное';
@override
String get listFilter_addToFavorites => 'Добавить в избранное';
@override
String get listFilter_removeFromFavorites => 'Удалить из избранного';
@override
String get listFilter_users => 'Пользователи';
@@ -2702,6 +2798,147 @@ class AppLocalizationsRu extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Одному или нескольким хмелям не указано местоположение!';
@override
String get pathTrace_clearTooltip => 'Очистить путь';
@override
String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.';
@override
String losRunFailed(String error) {
return 'Проверка прямой видимости не удалась: $error';
}
@override
String get losClearAllPoints => 'Очистить все точки';
@override
String get losRunToViewElevationProfile =>
'Запустите LOS, чтобы просмотреть профиль высот.';
@override
String get losMenuTitle => 'ЛОС Меню';
@override
String get losMenuSubtitle =>
'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.';
@override
String get losShowDisplayNodes => 'Показать узлы отображения';
@override
String get losCustomPoints => 'Пользовательские точки';
@override
String losCustomPointLabel(int index) {
return 'Пользовательский $index';
}
@override
String get losPointA => 'Точка А';
@override
String get losPointB => 'Точка Б';
@override
String losAntennaA(String value, String unit) {
return 'Антенна А: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Антенна Б: $value $unit';
}
@override
String get losRun => 'Запустить ЛОС';
@override
String get losNoElevationData => 'Нет данных о высоте';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, заблокирован $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'ЛОС: проверяю...';
@override
String get losStatusNoData => 'ЛОС: нет данных';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.';
}
@override
String get losErrorElevationUnavailable =>
'Данные о высоте недоступны для одного или нескольких образцов.';
@override
String get losErrorInvalidInput =>
'Неверные данные о точках/высоте для расчета LOS.';
@override
String get losRenameCustomPoint => 'Переименовать пользовательскую точку';
@override
String get losPointName => 'Имя точки';
@override
String get losShowPanelTooltip => 'Показать панель LOS';
@override
String get losHidePanelTooltip => 'Скрыть панель LOS';
@override
String get losElevationAttribution =>
'Данные о высоте: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радиогоризонт';
@override
String get losLegendLosBeam => 'Линия прямой видимости';
@override
String get losLegendTerrain => 'Рельеф';
@override
String get losFrequencyLabel => 'Частота';
@override
String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта';
@override
String get losFrequencyDialogTitle => 'Расчёт радиогоризонта';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.';
}
@override
String get contacts_pathTrace => 'Трассировка пути';
@@ -2880,4 +3117,10 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open экспорт данных карты GPX';
@override
String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы';
@override
String get snrIndicator_lastSeen => 'Последний раз видели';
}
+257 -15
View File
@@ -143,6 +143,16 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_scan => 'Skončiť';
@override
String get scanner_bluetoothOff => 'Bluetooth je vypnutý';
@override
String get scanner_bluetoothOffMessage =>
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
@override
String get scanner_enableBluetooth => 'Povolte Bluetooth';
@override
String get device_quickSwitch => 'Rýchle prepínač';
@@ -310,6 +320,10 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_aboutDescription =>
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Meno';
@@ -334,15 +348,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_presets => 'Prednastavenia';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvencia (MHz)';
@@ -371,10 +376,15 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)';
@override
String get settings_longRange => 'Dlhý dosah';
String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete';
@override
String get settings_fastSpeed => 'Rýchla rýchlosť';
String get settings_clientRepeatSubtitle =>
'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.';
@override
String get settings_clientRepeatFreqWarning =>
'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.';
@override
String settings_error(String message) {
@@ -450,6 +460,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrajinská';
@override
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Zobraziť podrobné metadáta o smerovaní a časovaní správ';
@override
String get appSettings_notifications => 'Upozornenia';
@@ -608,6 +625,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
@override
String get appSettings_unitsTitle => 'Jednotky';
@override
String get appSettings_unitsMetric => 'Metrické (m / km)';
@override
String get appSettings_unitsImperial => 'Imperiálne (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
@@ -645,7 +671,35 @@ class AppLocalizationsSk extends AppLocalizations {
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
@override
String get contacts_searchContacts => 'Vyhľadávajte kontakty...';
String get contacts_unread => 'Neprečítané';
@override
String get contacts_searchContactsNoNumber => 'Hľadať kontakty...';
@override
String contacts_searchContacts(int number, String str) {
return 'Vyhľadávajte kontakty...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Hľadať $number$str obľúbené...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Hľadať $number$str používateľov...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Hľadať $number$str opakovače...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Hľadaj $number$str serverov miestností...';
}
@override
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
@@ -773,6 +827,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channels_editChannel => 'Upraviť kanál';
@override
String get channels_muteChannel => 'Stlmiť kanál';
@override
String get channels_unmuteChannel => 'Zrušiť stlmenie kanála';
@override
String get channels_deleteChannel => 'Odstrániť kanál';
@@ -781,6 +841,11 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kanál \"$name\" sa nepodarilo odstrániť';
}
@override
String channels_channelDeleted(String name) {
return 'Kanál \"$name\" bol odstránený';
@@ -1068,6 +1133,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_pathManagement => 'Správa ciest';
@override
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
@override
String get chat_routingMode => 'Režim trasy';
@@ -1227,6 +1295,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_title => 'Mapa uzlov';
@override
String get map_lineOfSight => 'Line of Sight';
@override
String get map_losScreenTitle => 'Line of Sight';
@override
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
@@ -1356,6 +1430,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_manageRepeater => 'Spravovať Opakovanie';
@override
String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.';
@override
String get map_runTrace => 'Spustiť trasovaním cesty';
@override
String get map_removeLast => 'Odstrániť posledný';
@override
String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.';
@override
String get mapCache_title => 'Offline Mapa Pamäť';
@@ -1651,10 +1737,10 @@ class AppLocalizationsSk extends AppLocalizations {
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
@override
String get repeater_neighbours => 'Súsezný';
String get repeater_neighbors => 'Súsezný';
@override
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
@override
String get repeater_settings => 'Nastavenia';
@@ -2345,7 +2431,7 @@ class AppLocalizationsSk extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
@override
String get neighbors_noData =>
@@ -2654,6 +2740,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get listFilter_all => 'Všetko';
@override
String get listFilter_favorites => 'Obľúbené';
@override
String get listFilter_addToFavorites => 'Pridaj do obľúbených';
@override
String get listFilter_removeFromFavorites => 'Odstrániť z označení';
@override
String get listFilter_users => 'Používatelia';
@@ -2685,6 +2780,147 @@ class AppLocalizationsSk extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Jedna alebo viac chmeľov chýba lokalita!';
@override
String get pathTrace_clearTooltip => 'Zmazať cestu';
@override
String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.';
@override
String losRunFailed(String error) {
return 'Kontrola priamej viditeľnosti zlyhala: $error';
}
@override
String get losClearAllPoints => 'Vymazať všetky body';
@override
String get losRunToViewElevationProfile =>
'Ak chcete zobraziť výškový profil, spustite LOS';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body';
@override
String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia';
@override
String get losCustomPoints => 'Vlastné body';
@override
String losCustomPointLabel(int index) {
return 'Vlastné $index';
}
@override
String get losPointA => 'Bod A';
@override
String get losPointB => 'Bod B';
@override
String losAntennaA(String value, String unit) {
return 'Anténa A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Anténa B: $value $unit';
}
@override
String get losRun => 'Spustite LOS';
@override
String get losNoElevationData => 'Žiadne údaje o nadmorskej výške';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blokovaný $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: kontrolujem...';
@override
String get losStatusNoData => 'LOS: žiadne údaje';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme';
}
@override
String get losErrorElevationUnavailable =>
'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.';
@override
String get losErrorInvalidInput =>
'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.';
@override
String get losRenameCustomPoint => 'Premenovať vlastný bod';
@override
String get losPointName => 'Názov bodu';
@override
String get losShowPanelTooltip => 'Zobraziť panel LOS';
@override
String get losHidePanelTooltip => 'Skryť panel LOS';
@override
String get losElevationAttribution =>
'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Rádiový horizont';
@override
String get losLegendLosBeam => 'Priama viditeľnosť';
@override
String get losLegendTerrain => 'Terén';
@override
String get losFrequencyLabel => 'Frekvencia';
@override
String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu';
@override
String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.';
}
@override
String get contacts_pathTrace => 'Sledovanie lúčov';
@@ -2856,4 +3092,10 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open export dát GPX mapových údajov';
@override
String get snrIndicator_nearByRepeaters => 'Miestne opakovače';
@override
String get snrIndicator_lastSeen => 'Naposledy videný';
}
+257 -15
View File
@@ -143,6 +143,16 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get scanner_scan => 'Skeniraj';
@override
String get scanner_bluetoothOff => 'Bluetooth je izklopljen';
@override
String get scanner_bluetoothOffMessage =>
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
@override
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
@override
String get device_quickSwitch => 'Hitro preklop';
@@ -309,6 +319,10 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_aboutDescription =>
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Podatki o višini LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Ime';
@@ -333,15 +347,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_presets => 'Prednastavitve';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvenca (MHz)';
@@ -370,10 +375,15 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)';
@override
String get settings_longRange => 'DDolg doseg';
String get settings_clientRepeat => 'Neovadno ponavljanje';
@override
String get settings_fastSpeed => 'Visoka hitrost';
String get settings_clientRepeatSubtitle =>
'Omogočite temu naprave, da ponavlja paketne sporočila za druge.';
@override
String get settings_clientRepeatFreqWarning =>
'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.';
@override
String settings_error(String message) {
@@ -449,6 +459,13 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrajinsko';
@override
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil';
@override
String get appSettings_notifications => 'Obvestila';
@@ -609,6 +626,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
@override
String get appSettings_unitsTitle => 'Enote';
@override
String get appSettings_unitsMetric => 'Metrična (m/km)';
@override
String get appSettings_unitsImperial => 'Imperialno (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Območje ni izbrano';
@@ -646,7 +672,35 @@ class AppLocalizationsSl extends AppLocalizations {
'Stiki se bodo prikazali, ko se naprave oglasijo.';
@override
String get contacts_searchContacts => 'Iskanje stikov...';
String get contacts_unread => 'Neprebrano';
@override
String get contacts_searchContactsNoNumber => 'Iskanje stikov...';
@override
String contacts_searchContacts(int number, String str) {
return 'Iskanje stikov...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Iskanje $number$str priljubljenih...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Išči $number$str uporabnikov...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Išči $number$str ponavljalnike...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Išči $number$str strežnikov sob...';
}
@override
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
@@ -771,6 +825,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channels_editChannel => 'Uredi kanal';
@override
String get channels_muteChannel => 'Utišaj kanal';
@override
String get channels_unmuteChannel => 'Vklopi obvestila kanala';
@override
String get channels_deleteChannel => 'Pošlji kanal';
@@ -779,6 +839,11 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Izbrišem \"$name\"? To se ne da povrniti.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kanala $name ni bilo mogoče izbrisati';
}
@override
String channels_channelDeleted(String name) {
return 'Kanal \"$name\" izbrisan.';
@@ -1066,6 +1131,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get chat_pathManagement => 'Upravljanje poti';
@override
String get chat_ShowAllPaths => 'Prikaži vse poti';
@override
String get chat_routingMode => 'Navodilo za usmerjevalni način';
@@ -1222,6 +1290,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_title => 'Mapa omrežja';
@override
String get map_lineOfSight => 'Linija vida';
@override
String get map_losScreenTitle => 'Linija vida';
@override
String get map_noNodesWithLocation =>
'Nihče od notranjih elementov nima podatkov o lokaciji.';
@@ -1352,6 +1426,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_manageRepeater => 'Upravljajte Ponovitve';
@override
String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.';
@override
String get map_runTrace => 'Zaženi sledenje poti';
@override
String get map_removeLast => 'Odstrani Zadnji';
@override
String get map_pathTraceCancelled => 'Spremljanje poti je prekinjeno.';
@override
String get mapCache_title =>
'Omrezni predpomnilnik zemljeških zemljejevskih slik';
@@ -1650,10 +1736,10 @@ class AppLocalizationsSl extends AppLocalizations {
'Pošlji ukazne povelje na ponovitveno enoto.';
@override
String get repeater_neighbours => 'Sosedi';
String get repeater_neighbors => 'Sosedi';
@override
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.';
@override
String get repeater_settings => 'Nastavitve';
@@ -2349,7 +2435,7 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi';
@override
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
@@ -2657,6 +2743,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get listFilter_all => 'Vse';
@override
String get listFilter_favorites => 'Priljubljene';
@override
String get listFilter_addToFavorites => 'Dodaj v priljubljene';
@override
String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih';
@override
String get listFilter_users => 'Uporabniki';
@@ -2688,6 +2783,147 @@ class AppLocalizationsSl extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Ena ali več hmelju manjka lokacija!';
@override
String get pathTrace_clearTooltip => 'Počisti pot';
@override
String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.';
@override
String losRunFailed(String error) {
return 'Preverjanje vidnega polja ni uspelo: $error';
}
@override
String get losClearAllPoints => 'Počisti vse točke';
@override
String get losRunToViewElevationProfile =>
'Zaženite LOS za ogled višinskega profila';
@override
String get losMenuTitle => 'LOS meni';
@override
String get losMenuSubtitle =>
'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri';
@override
String get losShowDisplayNodes => 'Pokaži prikazna vozlišča';
@override
String get losCustomPoints => 'Točke po meri';
@override
String losCustomPointLabel(int index) {
return 'Po meri $index';
}
@override
String get losPointA => 'Točka A';
@override
String get losPointB => 'Točka B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Zaženi LOS';
@override
String get losNoElevationData => 'Ni podatkov o višini';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blokiral $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: preverjam ...';
@override
String get losStatusNoData => 'LOS: ni podatkov';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total jasno, $blocked blokirano, $unknown neznano';
}
@override
String get losErrorElevationUnavailable =>
'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.';
@override
String get losErrorInvalidInput =>
'Neveljavni podatki o točkah/višini za izračun LOS.';
@override
String get losRenameCustomPoint => 'Preimenujte točko po meri';
@override
String get losPointName => 'Ime točke';
@override
String get losShowPanelTooltip => 'Pokaži ploščo LOS';
@override
String get losHidePanelTooltip => 'Skrij ploščo LOS';
@override
String get losElevationAttribution =>
'Podatki o višini: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radijski horizont';
@override
String get losLegendLosBeam => 'Linija vidnosti';
@override
String get losLegendTerrain => 'Teren';
@override
String get losFrequencyLabel => 'Frekvenca';
@override
String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna';
@override
String get losFrequencyDialogTitle => 'Izračun radijskega horizonta';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.';
}
@override
String get contacts_pathTrace => 'Sledenje poti';
@@ -2861,4 +3097,10 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open izvoz podatkov GPX karte';
@override
String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji';
@override
String get snrIndicator_lastSeen => 'Zadnjič videno';
}
+255 -15
View File
@@ -142,6 +142,16 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_scan => 'Skanna';
@override
String get scanner_bluetoothOff => 'Bluetooth är avstängt';
@override
String get scanner_bluetoothOffMessage =>
'Vänligen aktivera Bluetooth för att söka efter enheter.';
@override
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
@override
String get device_quickSwitch => 'Snabb växling';
@@ -307,6 +317,10 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_aboutDescription =>
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS-höjddata: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Namn';
@@ -331,15 +345,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_presets => 'Fördefinierade inställningar';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvens (MHz)';
@@ -368,10 +373,15 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)';
@override
String get settings_longRange => 'Lång räckvidd';
String get settings_clientRepeat => 'Upprepa utan elnät';
@override
String get settings_fastSpeed => 'Snabb hastighet';
String get settings_clientRepeatSubtitle =>
'Låt enheten repetera nätpaket för andra användare.';
@override
String get settings_clientRepeatFreqWarning =>
'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.';
@override
String settings_error(String message) {
@@ -447,6 +457,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrainska';
@override
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden';
@override
String get appSettings_notifications => 'Meddelanden';
@@ -604,6 +621,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Kartcache';
@override
String get appSettings_unitsTitle => 'Enheter';
@override
String get appSettings_unitsMetric => 'Metriskt (m/km)';
@override
String get appSettings_unitsImperial => 'Imperialt (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Ingen area markerad';
@@ -641,7 +667,35 @@ class AppLocalizationsSv extends AppLocalizations {
'Kontakter kommer att visas när enheter annonserar.';
@override
String get contacts_searchContacts => 'Sök kontakter...';
String get contacts_unread => 'Oläst';
@override
String get contacts_searchContactsNoNumber => 'Sök kontakter...';
@override
String contacts_searchContacts(int number, String str) {
return 'Sök kontakter...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Sök $number$str Favoriter...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Sök $number$str användare...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Sök $number$str upprepningsenheter...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Sök $number$str Room-servrar...';
}
@override
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
@@ -767,6 +821,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channels_editChannel => 'Redigera kanal';
@override
String get channels_muteChannel => 'Tysta kanal';
@override
String get channels_unmuteChannel => 'Slå på ljud för kanal';
@override
String get channels_deleteChannel => 'Ta bort kanal';
@@ -775,6 +835,11 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Radera \"$name\"? Detta kan inte ångras.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Det gick inte att ta bort kanalen \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Kanalen \"$name\" raderad';
@@ -1063,6 +1128,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get chat_pathManagement => 'Stigarhantering';
@override
String get chat_ShowAllPaths => 'Visa alla vägar';
@override
String get chat_routingMode => 'Ruttläge';
@@ -1219,6 +1287,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_title => 'Nodkarta';
@override
String get map_lineOfSight => 'Synlinje';
@override
String get map_losScreenTitle => 'Synlinje';
@override
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
@@ -1348,6 +1422,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_manageRepeater => 'Hantera Upprepare';
@override
String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.';
@override
String get map_runTrace => 'Kör spårsökning';
@override
String get map_removeLast => 'Ta bort sista';
@override
String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.';
@override
String get mapCache_title => 'Offline Kartcache';
@@ -1640,10 +1726,10 @@ class AppLocalizationsSv extends AppLocalizations {
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
@override
String get repeater_neighbours => 'Grannar';
String get repeater_neighbors => 'Grannar';
@override
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
@override
String get repeater_settings => 'Inställningar';
@@ -2334,7 +2420,7 @@ class AppLocalizationsSv extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
@override
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
@@ -2642,6 +2728,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get listFilter_all => 'Alla';
@override
String get listFilter_favorites => 'Favoriter';
@override
String get listFilter_addToFavorites => 'Lägg till i favoriter';
@override
String get listFilter_removeFromFavorites => 'Ta bort från favoriter';
@override
String get listFilter_users => 'Användare';
@@ -2673,6 +2768,145 @@ class AppLocalizationsSv extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'En eller flera av humlen saknar en plats!';
@override
String get pathTrace_clearTooltip => 'Rensa väg';
@override
String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.';
@override
String losRunFailed(String error) {
return 'Synlinjekontroll misslyckades: $error';
}
@override
String get losClearAllPoints => 'Rensa alla punkter';
@override
String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil';
@override
String get losMenuTitle => 'LOS-menyn';
@override
String get losMenuSubtitle =>
'Tryck på noder eller tryck länge på kartan för anpassade punkter';
@override
String get losShowDisplayNodes => 'Visa displaynoder';
@override
String get losCustomPoints => 'Anpassade poäng';
@override
String losCustomPointLabel(int index) {
return 'Anpassad $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antenn A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenn B: $value $unit';
}
@override
String get losRun => 'Kör LOS';
@override
String get losNoElevationData => 'Inga höjddata';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blockerad av $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: kollar...';
@override
String get losStatusNoData => 'LOS: inga data';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd';
}
@override
String get losErrorElevationUnavailable =>
'Höjddata är inte tillgänglig för ett eller flera prover.';
@override
String get losErrorInvalidInput =>
'Ogiltiga poäng/höjddata för LOS-beräkning.';
@override
String get losRenameCustomPoint => 'Byt namn på anpassad punkt';
@override
String get losPointName => 'Punktnamn';
@override
String get losShowPanelTooltip => 'Visa LOS-panelen';
@override
String get losHidePanelTooltip => 'Dölj LOS-panelen';
@override
String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radiohorisont';
@override
String get losLegendLosBeam => 'Siktlinje';
@override
String get losLegendTerrain => 'Terräng';
@override
String get losFrequencyLabel => 'Frekvens';
@override
String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen';
@override
String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.';
}
@override
String get contacts_pathTrace => 'Path Trace';
@@ -2841,4 +3075,10 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open export av GPX-kartdata';
@override
String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer';
@override
String get snrIndicator_lastSeen => 'Senast sedd';
}
+259 -15
View File
@@ -143,6 +143,16 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get scanner_scan => 'Сканувати';
@override
String get scanner_bluetoothOff => 'Bluetooth вимкнено';
@override
String get scanner_bluetoothOffMessage =>
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
@override
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
@override
String get device_quickSwitch => 'Швидке перемикання';
@@ -312,6 +322,10 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_aboutDescription =>
'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Дані про висоту LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Ім\'я';
@@ -336,15 +350,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_presets => 'Попередні налаштування';
@override
String get settings_preset915Mhz => '915 МГц';
@override
String get settings_preset868Mhz => '868 МГц';
@override
String get settings_preset433Mhz => '433 МГц';
@override
String get settings_frequency => 'Частота (МГц)';
@@ -373,10 +378,15 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)';
@override
String get settings_longRange => 'Дальній діапазон';
String get settings_clientRepeat => 'Автономна система';
@override
String get settings_fastSpeed => 'Висока швидкість';
String get settings_clientRepeatSubtitle =>
'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.';
@override
String get settings_clientRepeatFreqWarning =>
'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.';
@override
String settings_error(String message) {
@@ -452,6 +462,14 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_enableMessageTracing =>
'Увімкнути відстеження повідомлень';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Показувати детальні метадані про маршрутизацію та час для повідомлень';
@override
String get appSettings_notifications => 'Сповіщення';
@@ -612,6 +630,15 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
@override
String get appSettings_unitsTitle => 'одиниці';
@override
String get appSettings_unitsMetric => 'Метричний (м / км)';
@override
String get appSettings_unitsImperial => 'Імперська (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Область не вибрано';
@@ -649,7 +676,35 @@ class AppLocalizationsUk extends AppLocalizations {
'Контакти з\'являться, коли пристрої надішлють оголошення.';
@override
String get contacts_searchContacts => 'Пошук контактів...';
String get contacts_unread => 'Непрочитане';
@override
String get contacts_searchContactsNoNumber => 'Пошук контактів...';
@override
String contacts_searchContacts(int number, String str) {
return 'Пошук контактів...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Пошук $number$str улюблених...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Пошук $number$str користувачів...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Пошук $number$str ретрансляторів...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Пошук $number$str серверів кімнат...';
}
@override
String get contacts_noUnreadContacts => 'Немає непрочитаних контактів';
@@ -774,6 +829,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get channels_editChannel => 'Редагувати канал';
@override
String get channels_muteChannel => 'Вимкнути сповіщення каналу';
@override
String get channels_unmuteChannel => 'Увімкнути сповіщення каналу';
@override
String get channels_deleteChannel => 'Видалити канал';
@@ -782,6 +843,11 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Видалити $name? Це не можна скасувати.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Не вдалося видалити канал \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Канал «$name» видалено';
@@ -1069,6 +1135,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get chat_pathManagement => 'Керування шляхами';
@override
String get chat_ShowAllPaths => 'Показати всі шляхи';
@override
String get chat_routingMode => 'Режим маршрутизації';
@@ -1231,6 +1300,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_title => 'Карта вузлів';
@override
String get map_lineOfSight => 'Пряма видимість';
@override
String get map_losScreenTitle => 'Пряма видимість';
@override
String get map_noNodesWithLocation =>
'Немає вузлів з даними про розташування';
@@ -1361,6 +1436,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_manageRepeater => 'Керувати ретранслятором';
@override
String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху';
@override
String get map_runTrace => 'Виконати трасування шляху';
@override
String get map_removeLast => 'Видалити останній';
@override
String get map_pathTraceCancelled => 'Відмінується трасування шляху';
@override
String get mapCache_title => 'Офлайн-кеш карти';
@@ -1657,10 +1744,10 @@ class AppLocalizationsUk extends AppLocalizations {
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
@override
String get repeater_neighbours => 'Сусіди';
String get repeater_neighbors => 'Сусіди';
@override
String get repeater_neighboursSubtitle =>
String get repeater_neighborsSubtitle =>
'Показати сусідів нульового стрибка.';
@override
@@ -2362,7 +2449,7 @@ class AppLocalizationsUk extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди';
String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди';
@override
String get neighbors_noData => 'Дані про сусідів недоступні.';
@@ -2678,6 +2765,15 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get listFilter_all => 'Все';
@override
String get listFilter_favorites => 'Улюблені';
@override
String get listFilter_addToFavorites => 'Додати до улюблених';
@override
String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених';
@override
String get listFilter_users => 'Користувачі';
@@ -2709,6 +2805,148 @@ class AppLocalizationsUk extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Одне або більше хмелів відсутнє місце розташування!';
@override
String get pathTrace_clearTooltip => 'Очистити шлях';
@override
String get losSelectStartEnd =>
'Виберіть початковий і кінцевий вузли для LOS.';
@override
String losRunFailed(String error) {
return 'Помилка перевірки прямої видимості: $error';
}
@override
String get losClearAllPoints => 'Очистити всі пункти';
@override
String get losRunToViewElevationProfile =>
'Запустіть LOS, щоб переглянути профіль висоти';
@override
String get losMenuTitle => 'Меню LOS';
@override
String get losMenuSubtitle =>
'Торкніться вузлів або утримуйте карту, щоб отримати власні точки';
@override
String get losShowDisplayNodes => 'Показати вузли відображення';
@override
String get losCustomPoints => 'Користувальницькі точки';
@override
String losCustomPointLabel(int index) {
return 'Спеціальний $index';
}
@override
String get losPointA => 'Точка А';
@override
String get losPointB => 'Точка Б';
@override
String losAntennaA(String value, String unit) {
return 'Антена A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Антена B: $value $unit';
}
@override
String get losRun => 'Запустіть LOS';
@override
String get losNoElevationData => 'Немає даних про висоту';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, заблоковано $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: перевірка...';
@override
String get losStatusNoData => 'LOS: немає даних';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо';
}
@override
String get losErrorElevationUnavailable =>
'Дані про висоту недоступні для одного чи кількох зразків.';
@override
String get losErrorInvalidInput =>
'Недійсні дані про точки/висоту для розрахунку LOS.';
@override
String get losRenameCustomPoint => 'Перейменуйте спеціальну точку';
@override
String get losPointName => 'Назва точки';
@override
String get losShowPanelTooltip => 'Показати панель LOS';
@override
String get losHidePanelTooltip => 'Приховати панель LOS';
@override
String get losElevationAttribution =>
'Дані про висоту: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радіогоризонт';
@override
String get losLegendLosBeam => 'Лінія прямої видимості';
@override
String get losLegendTerrain => 'Рельєф';
@override
String get losFrequencyLabel => 'Частота';
@override
String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку';
@override
String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.';
}
@override
String get contacts_pathTrace => 'Трасування шляхів';
@@ -2886,4 +3124,10 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'експорт даних карти meshcore-open у форматі GPX';
@override
String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори';
@override
String get snrIndicator_lastSeen => 'Останній раз бачили';
}
File diff suppressed because it is too large Load Diff
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "nl",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacten",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Aantal Contacten",
"settings_infoChannelCount": "Aantal Kanalen",
"settings_presets": "Presets",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequentie (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Vermogen (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
"settings_longRange": "Lange Afstand",
"settings_fastSpeed": "Hoge Snelheid",
"settings_error": "Fout: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Open kanaal",
"channels_privateChannel": "Private kanaal",
"channels_editChannel": "Kanaal bewerken",
"channels_muteChannel": "Kanaal dempen",
"channels_unmuteChannel": "Kanaal dempen opheffen",
"channels_deleteChannel": "Kanaal verwijderen",
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighbours": "Buren",
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
"repeater_neighbors": "Buren",
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
"neighbors_receivedData": "Ontvangen Buurdata",
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
"neighbors_repeatersNeighbours": "Herhalingen Buren",
"neighbors_repeatersNeighbors": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
@@ -1560,6 +1565,8 @@
"contacts_floodAdvert": "Overstromingsadvertentie",
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
"appSettings_languageRu": "Russisch",
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
"contacts_clipboardEmpty": "Knipbord is leeg.",
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
"contacts_contactImported": "Contact is geïmporteerd.",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}",
"notification_newTypeDiscovered": "Nieuw {contactType} ontdekt",
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportAllContacts": "Alle contactlocaties",
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!"
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
"map_removeLast": "Verwijder Laatste",
"pathTrace_clearTooltip": "Weg wissen",
"map_pathTraceCancelled": "Pad traceren geannuleerd",
"map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad",
"map_runTrace": "Padeshulp traceren",
"scanner_enableBluetooth": "Activeer Bluetooth",
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
"snrIndicator_lastSeen": "Laatst gezien",
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
"chat_ShowAllPaths": "Toon alle paden",
"settings_clientRepeat": "Herhalen: Afgekoppeld",
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Eenheden",
"appSettings_unitsMetric": "Metrisch (m / km)",
"appSettings_unitsImperial": "Imperiaal (ft / mi)",
"map_lineOfSight": "Zichtlijn",
"map_losScreenTitle": "Zichtlijn",
"losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.",
"losRunFailed": "Zichtlijncontrole mislukt: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Wis alle punten",
"losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken",
"losMenuTitle": "LOS-menu",
"losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten",
"losShowDisplayNodes": "Toon weergaveknooppunten",
"losCustomPoints": "Aangepaste punten",
"losCustomPointLabel": "Aangepast {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punt A",
"losPointB": "Punt B",
"losAntennaA": "Antenne A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Voer LOS uit",
"losNoElevationData": "Geen hoogtegegevens",
"losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: controleren...",
"losStatusNoData": "LOS: geen gegevens",
"losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.",
"losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.",
"losRenameCustomPoint": "Hernoem aangepast punt",
"losPointName": "Puntnaam",
"losShowPanelTooltip": "Toon LOS-paneel",
"losHidePanelTooltip": "LOS-paneel verbergen",
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radiohorizon",
"losLegendLosBeam": "Zichtlijn",
"losLegendTerrain": "Terrein",
"losFrequencyLabel": "Frequentie",
"losFrequencyInfoTooltip": "Bekijk details van de berekening",
"losFrequencyDialogTitle": "Berekening van de radiohorizon",
"losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Verwijderen uit favorieten",
"listFilter_favorites": "Favorieten",
"listFilter_addToFavorites": "Toevoegen aan favorieten",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Ongelezen",
"contacts_searchRepeaters": "Zoek {number}{str} Repeaters...",
"contacts_searchContactsNoNumber": "Zoek contacten...",
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "pl",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakty",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Liczba kontaktów",
"settings_infoChannelCount": "Liczba kanałów",
"settings_presets": "Preset",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Częstotliwość (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Moc (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)",
"settings_longRange": "Długi zasięg",
"settings_fastSpeed": "Szybka prędkość",
"settings_error": "Błąd: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Kanał publiczny",
"channels_privateChannel": "Prywatny kanał",
"channels_editChannel": "Edytuj kanał",
"channels_muteChannel": "Wycisz kanał",
"channels_unmuteChannel": "Wyłącz wyciszenie kanału",
"channels_deleteChannel": "Usuń kanał",
"channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighbours": "Sąsiedzi",
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"repeater_neighbors": "Sąsiedzi",
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
"neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
@@ -1555,6 +1560,8 @@
"contacts_chatTraceRoute": "Śledź trasę promienia",
"appSettings_languageRu": "Rosyjski",
"appSettings_languageUk": "Ukraińska",
"appSettings_enableMessageTracing": "Włącz śledzenie wiadomości",
"appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości",
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
"contacts_zeroHopAdvert": "Reklama Zero Hop",
"contacts_floodAdvert": "Reklama powodziowa",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}",
"notification_newTypeDiscovered": "Nowy {contactType} wykryty",
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
"contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.",
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
"settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX",
"settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportChat": "Lokalizacje towarzyszy",
"settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open",
"settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open",
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!"
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!",
"map_pathTraceCancelled": "Śledzenie ścieżki anulowano.",
"map_runTrace": "Uruchom ślad ścieżki",
"pathTrace_clearTooltip": "Wyczyść ścieżkę",
"map_removeLast": "Usuń ostatni",
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
"scanner_enableBluetooth": "Włącz Bluetooth",
"snrIndicator_lastSeen": "Ostatnio widziany",
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.",
"settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Jednostki",
"appSettings_unitsMetric": "Metryczne (m / km)",
"appSettings_unitsImperial": "Imperialne (ft / mi)",
"map_lineOfSight": "Linia wzroku",
"map_losScreenTitle": "Linia wzroku",
"losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.",
"losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Wyczyść wszystkie punkty",
"losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty",
"losShowDisplayNodes": "Pokaż węzły wyświetlające",
"losCustomPoints": "Punkty niestandardowe",
"losCustomPointLabel": "Niestandardowe {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Uruchom LOS-a",
"losNoElevationData": "Brak danych o wysokości",
"losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: sprawdzam...",
"losStatusNoData": "LOS: brak danych",
"losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.",
"losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.",
"losRenameCustomPoint": "Zmień nazwę punktu niestandardowego",
"losPointName": "Nazwa punktu",
"losShowPanelTooltip": "Pokaż panel LOS",
"losHidePanelTooltip": "Ukryj panel LOS",
"losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horyzont radiowy",
"losLegendLosBeam": "Linia widoczności",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Częstotliwość",
"losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia",
"losFrequencyDialogTitle": "Obliczanie horyzontu radiowego",
"losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Usuń z ulubionych",
"listFilter_addToFavorites": "Dodaj do ulubionych",
"listFilter_favorites": "Ulubione",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Nieprzeczytane",
"contacts_searchContactsNoNumber": "Wyszukaj kontakty...",
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..."
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "pt",
"appTitle": "MeshCore Open",
"nav_contacts": "Contactos",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Número de Contatos",
"settings_infoChannelCount": "Número do Canal",
"settings_presets": "Presets",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequência (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Potência (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)",
"settings_longRange": "Alcance Longo",
"settings_fastSpeed": "Velocidade Rápida",
"settings_error": "Erro: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Canal público",
"channels_privateChannel": "Canal privado",
"channels_editChannel": "Editar canal",
"channels_muteChannel": "Silenciar canal",
"channels_unmuteChannel": "Ativar canal",
"channels_deleteChannel": "Excluir canal",
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighbours": "Vizinhos",
"repeater_neighbors": "Vizinhos",
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
"repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.",
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
"neighbors_repeatersNeighbors": "Repetidores Vizinhos",
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
@@ -1561,6 +1566,8 @@
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
"appSettings_languageRu": "Russo",
"appSettings_enableMessageTracing": "Ativar rastreamento de mensagens",
"appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens",
"contacts_ShareContact": "Copiar contato para Área de Transferência",
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}",
"notification_newTypeDiscovered": "Novo {contactType} descoberto",
"notification_receivedNewMessage": "Nova mensagem recebida",
"contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.",
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX",
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.",
"settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportAllContacts": "Todos os locais de contatos",
"settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX",
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!"
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!",
"map_runTrace": "Executar Traçado de Caminho",
"map_pathTraceCancelled": "Rastreamento de caminho cancelado.",
"pathTrace_clearTooltip": "Limpar caminho",
"map_removeLast": "Remover Último",
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
"scanner_enableBluetooth": "Ative o Bluetooth",
"scanner_bluetoothOff": "Bluetooth está desativado",
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
"snrIndicator_lastSeen": "Visto pela última vez",
"chat_ShowAllPaths": "Mostrar todos os caminhos",
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
"settings_clientRepeat": "Repetição sem rede",
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.",
"settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unidades",
"appSettings_unitsMetric": "Métrico (m/km)",
"appSettings_unitsImperial": "Imperial (ft/mi)",
"map_lineOfSight": "Linha de visão",
"map_losScreenTitle": "Linha de visão",
"losSelectStartEnd": "Selecione nós iniciais e finais para LOS.",
"losRunFailed": "Falha na verificação da linha de visão: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Limpe todos os pontos",
"losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados",
"losShowDisplayNodes": "Mostrar nós de exibição",
"losCustomPoints": "Pontos personalizados",
"losCustomPointLabel": "{index} personalizado",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Ponto A",
"losPointB": "Ponto B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Executar LOS",
"losNoElevationData": "Sem dados de elevação",
"losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: verificando...",
"losStatusNoData": "LOS: sem dados",
"losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.",
"losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.",
"losRenameCustomPoint": "Renomear ponto personalizado",
"losPointName": "Nome do ponto",
"losShowPanelTooltip": "Mostrar painel LOS",
"losHidePanelTooltip": "Ocultar painel LOS",
"losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizonte de rádio",
"losLegendLosBeam": "Linha de visada",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequência",
"losFrequencyInfoTooltip": "Ver detalhes do cálculo",
"losFrequencyDialogTitle": "Cálculo do horizonte de rádio",
"losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Adicionar aos favoritos",
"listFilter_removeFromFavorites": "Remover da lista de favoritos",
"listFilter_favorites": "Favoritos",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...",
"contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...",
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
"contacts_unread": "Não lido",
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Не удалось удалить канал {name}.",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "ru",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакты",
@@ -101,9 +109,6 @@
"settings_infoContactsCount": "Количество контактов",
"settings_infoChannelCount": "Количество каналов",
"settings_presets": "Пресеты",
"settings_preset915Mhz": "915 МГц",
"settings_preset868Mhz": "868 МГц",
"settings_preset433Mhz": "433 МГц",
"settings_frequency": "Частота (МГц)",
"settings_frequencyHelper": "300.0 2500.0",
"settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)",
@@ -113,8 +118,6 @@
"settings_txPower": "Мощность передачи (дБм)",
"settings_txPowerHelper": "0 22",
"settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)",
"settings_longRange": "Дальний радиус",
"settings_fastSpeed": "Высокая скорость",
"settings_error": "Ошибка: {message}",
"appSettings_title": "Настройки приложения",
"appSettings_appearance": "Внешний вид",
@@ -231,6 +234,8 @@
"channels_publicChannel": "Публичный канал",
"channels_privateChannel": "Приватный канал",
"channels_editChannel": "Изменить канал",
"channels_muteChannel": "Отключить уведомления канала",
"channels_unmuteChannel": "Включить уведомления канала",
"channels_deleteChannel": "Удалить канал",
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
"channels_channelDeleted": "Канал \"{name}\" удалён",
@@ -472,8 +477,8 @@
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Отправка команд репитеру",
"repeater_neighbours": "Соседи",
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
"repeater_neighbors": "Соседи",
"repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.",
"repeater_settings": "Настройки",
"repeater_settingsSubtitle": "Настройка параметров репитера",
"repeater_statusTitle": "Статус репитера",
@@ -666,7 +671,7 @@
"neighbors_receivedData": "Полученные данные о соседях",
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
"neighbors_repeatersNeighbours": "Соседи репитеров",
"neighbors_repeatersNeighbors": "Соседи репитеров",
"neighbors_noData": "Данные о соседях недоступны.",
"neighbors_unknownContact": "Неизвестный {pubkey}",
"neighbors_heardA ago": "Слышали: {time} назад",
@@ -799,6 +804,8 @@
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
"contacts_zeroHopAdvert": "Реклама Zero Hop",
"appSettings_languageUk": "Українська",
"appSettings_enableMessageTracing": "Включить трассировку сообщений",
"appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений",
"contacts_floodAdvert": "Рекламный поток",
"contacts_clipboardEmpty": "Буфер обмена пуст.",
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
@@ -815,7 +822,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}",
"notification_newTypeDiscovered": "Обнаружен новый {contactType}",
"notification_receivedNewMessage": "Получено новое сообщение",
"contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.",
"settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX",
"settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.",
"settings_gpxExportContacts": "Экспортировать спутников в GPX",
@@ -831,6 +837,207 @@
"settings_gpxExportNoContacts": "Нет контактов для экспорта.",
"settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX",
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!"
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!",
"map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.",
"map_removeLast": "Удалить последний",
"map_pathTraceCancelled": "Отмена трассировки пути",
"pathTrace_clearTooltip": "Очистить путь",
"map_runTrace": "Запустить трассировку пути",
"scanner_enableBluetooth": "Включите Bluetooth",
"scanner_bluetoothOff": "Bluetooth выключен",
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
"snrIndicator_lastSeen": "Последний раз видели",
"chat_ShowAllPaths": "Показать все пути",
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
"settings_clientRepeat": "Повторение \"вне сети\"",
"settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Единицы",
"appSettings_unitsMetric": "Метрическая (м/км)",
"appSettings_unitsImperial": "Имперская (ft / mi)",
"map_lineOfSight": "Линия видимости",
"map_losScreenTitle": "Линия видимости",
"losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.",
"losRunFailed": "Проверка прямой видимости не удалась: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Очистить все точки",
"losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.",
"losMenuTitle": "ЛОС Меню",
"losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.",
"losShowDisplayNodes": "Показать узлы отображения",
"losCustomPoints": "Пользовательские точки",
"losCustomPointLabel": "Пользовательский {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Точка А",
"losPointB": "Точка Б",
"losAntennaA": "Антенна А: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Антенна Б: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Запустить ЛОС",
"losNoElevationData": "Нет данных о высоте",
"losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "ЛОС: проверяю...",
"losStatusNoData": "ЛОС: нет данных",
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.",
"losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.",
"losRenameCustomPoint": "Переименовать пользовательскую точку",
"losPointName": "Имя точки",
"losShowPanelTooltip": "Показать панель LOS",
"losHidePanelTooltip": "Скрыть панель LOS",
"losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Радиогоризонт",
"losLegendLosBeam": "Линия прямой видимости",
"losLegendTerrain": "Рельеф",
"losFrequencyLabel": "Частота",
"losFrequencyInfoTooltip": "Просмотреть детали расчёта",
"losFrequencyDialogTitle": "Расчёт радиогоризонта",
"losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Добавить в избранное",
"listFilter_favorites": "Избранное",
"listFilter_removeFromFavorites": "Удалить из избранного",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...",
"contacts_searchContactsNoNumber": "Поиск контактов...",
"contacts_unread": "Непрочитанное",
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sk",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakty",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Počet kontaktov",
"settings_infoChannelCount": "Počet kanálov",
"settings_presets": "Prednastavenia",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frekvencia (MHz)",
"settings_frequencyHelper": "300,0 2500,0",
"settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Výkon (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)",
"settings_longRange": "Dlhý dosah",
"settings_fastSpeed": "Rýchla rýchlosť",
"settings_error": "Chyba: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Veľké verejne kanály",
"channels_privateChannel": "Osobné kanál",
"channels_editChannel": "Upraviť kanál",
"channels_muteChannel": "Stlmiť kanál",
"channels_unmuteChannel": "Zrušiť stlmenie kanála",
"channels_deleteChannel": "Odstrániť kanál",
"channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
"neighbors_receivedData": "Obdielo dáta suseda",
"repeater_neighbours": "Súsezný",
"repeater_neighbors": "Súsezný",
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
"neighbors_repeatersNeighbors": "Opakovadlá Súsezná",
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
@@ -1561,6 +1566,8 @@
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
"appSettings_languageRu": "Ruština",
"appSettings_enableMessageTracing": "Povoliť sledovanie správ",
"appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ",
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
"contacts_contactImported": "Kontakt bol importovaný.",
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}",
"notification_newTypeDiscovered": "Nový {contactType} objavený",
"notification_receivedNewMessage": "Prijatá nová správa",
"contacts_ShareContact": "Kopírovať kontakt do schránky",
"settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.",
"settings_gpxExportContacts": "Export sprievodcov do GPX",
"settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportChat": "Lokácie sprievodcov",
"settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov",
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!"
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!",
"pathTrace_clearTooltip": "Zmazať cestu",
"map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.",
"map_removeLast": "Odstrániť posledný",
"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_bluetoothOff": "Bluetooth je vypnutý",
"scanner_enableBluetooth": "Povolte Bluetooth",
"snrIndicator_lastSeen": "Naposledy videný",
"snrIndicator_nearByRepeaters": "Miestne opakovače",
"chat_ShowAllPaths": "Zobraziť všetky cesty",
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.",
"settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Jednotky",
"appSettings_unitsMetric": "Metrické (m / km)",
"appSettings_unitsImperial": "Imperiálne (ft / mi)",
"map_lineOfSight": "Line of Sight",
"map_losScreenTitle": "Line of Sight",
"losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.",
"losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Vymazať všetky body",
"losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body",
"losShowDisplayNodes": "Zobraziť uzly zobrazenia",
"losCustomPoints": "Vlastné body",
"losCustomPointLabel": "Vlastné {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Bod A",
"losPointB": "Bod B",
"losAntennaA": "Anténa A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Anténa B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Spustite LOS",
"losNoElevationData": "Žiadne údaje o nadmorskej výške",
"losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: kontrolujem...",
"losStatusNoData": "LOS: žiadne údaje",
"losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.",
"losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.",
"losRenameCustomPoint": "Premenovať vlastný bod",
"losPointName": "Názov bodu",
"losShowPanelTooltip": "Zobraziť panel LOS",
"losHidePanelTooltip": "Skryť panel LOS",
"losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Rádiový horizont",
"losLegendLosBeam": "Priama viditeľnosť",
"losLegendTerrain": "Terén",
"losFrequencyLabel": "Frekvencia",
"losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu",
"losFrequencyDialogTitle": "Výpočet rádiového horizontu",
"losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Odstrániť z označení",
"listFilter_addToFavorites": "Pridaj do obľúbených",
"listFilter_favorites": "Obľúbené",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...",
"contacts_searchFavorites": "Hľadať {number}{str} obľúbené...",
"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é"
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sl",
"appTitle": "MeshCore Open",
"nav_contacts": "Stiki",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Število stikov",
"settings_infoChannelCount": "Število kanalov",
"settings_presets": "Prednastavitve",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frekvenca (MHz)",
"settings_frequencyHelper": "300,00 - 2500,00",
"settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX Moč (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
"settings_longRange": "DDolg doseg",
"settings_fastSpeed": "Visoka hitrost",
"settings_error": "Napaka: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Javni kanal",
"channels_privateChannel": "Zasebni kanal",
"channels_editChannel": "Uredi kanal",
"channels_muteChannel": "Utišaj kanal",
"channels_unmuteChannel": "Vklopi obvestila kanala",
"channels_deleteChannel": "Pošlji kanal",
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbours": "Sosedi",
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbors": "Sosedi",
"neighbors_receivedData": "Prejeto podatke o sosedih",
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
"neighbors_noData": "Niso na voljo podatki o sosedih.",
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
@@ -1555,6 +1560,8 @@
"contacts_pathTraceTo": "Trace route to {name}",
"appSettings_languageRu": "Ruščina",
"appSettings_languageUk": "Ukrajinsko",
"appSettings_enableMessageTracing": "Omogoči sledenje sporočilom",
"appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil",
"contacts_contactImported": "Kontakt je bil uvožen.",
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}",
"notification_newTypeDiscovered": "Odkrito novo {contactType}",
"notification_receivedNewMessage": "Prejeto novo sporočilo",
"contacts_ShareContact": "Kopiraj stik v Odložišče",
"settings_gpxExportAll": "Izvozi vse kontakte v GPX",
"settings_gpxExportContacts": "Izvoz spremljevalcev v GPX",
"settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportNoContacts": "Ni stikov za izvoz.",
"settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu",
"settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte",
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!"
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!",
"map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.",
"map_removeLast": "Odstrani Zadnji",
"map_runTrace": "Zaženi sledenje poti",
"pathTrace_clearTooltip": "Počisti pot",
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
"scanner_enableBluetooth": "Omogočite Bluetooth",
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
"scanner_bluetoothOff": "Bluetooth je izklopljen",
"snrIndicator_lastSeen": "Zadnjič videno",
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
"chat_ShowAllPaths": "Prikaži vse poti",
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
"settings_clientRepeat": "Neovadno ponavljanje",
"settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Enote",
"appSettings_unitsMetric": "Metrična (m/km)",
"appSettings_unitsImperial": "Imperialno (ft / mi)",
"map_lineOfSight": "Linija vida",
"map_losScreenTitle": "Linija vida",
"losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.",
"losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Počisti vse točke",
"losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila",
"losMenuTitle": "LOS meni",
"losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri",
"losShowDisplayNodes": "Pokaži prikazna vozlišča",
"losCustomPoints": "Točke po meri",
"losCustomPointLabel": "Po meri {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Točka A",
"losPointB": "Točka B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Zaženi LOS",
"losNoElevationData": "Ni podatkov o višini",
"losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: preverjam ...",
"losStatusNoData": "LOS: ni podatkov",
"losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.",
"losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.",
"losRenameCustomPoint": "Preimenujte točko po meri",
"losPointName": "Ime točke",
"losShowPanelTooltip": "Pokaži ploščo LOS",
"losHidePanelTooltip": "Skrij ploščo LOS",
"losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radijski horizont",
"losLegendLosBeam": "Linija vidnosti",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Frekvenca",
"losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna",
"losFrequencyDialogTitle": "Izračun radijskega horizonta",
"losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_favorites": "Priljubljene",
"listFilter_removeFromFavorites": "Odstrani iz priljubljenih",
"listFilter_addToFavorites": "Dodaj v priljubljene",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Neprebrano",
"contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...",
"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..."
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sv",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakter",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Kontakterantal",
"settings_infoChannelCount": "Kanalantal",
"settings_presets": "Fördefinierade inställningar",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frekvens (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)",
@@ -143,8 +148,6 @@
"settings_txPower": "TX-effekt (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)",
"settings_longRange": "Lång räckvidd",
"settings_fastSpeed": "Snabb hastighet",
"settings_error": "Fel: {message}",
"@settings_error": {
"placeholders": {
@@ -339,6 +342,8 @@
"channels_publicChannel": "Allmänt kanal",
"channels_privateChannel": "Privat kanal",
"channels_editChannel": "Redigera kanal",
"channels_muteChannel": "Tysta kanal",
"channels_unmuteChannel": "Slå på ljud för kanal",
"channels_deleteChannel": "Ta bort kanal",
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
"@channels_deleteChannelConfirm": {
@@ -1356,12 +1361,12 @@
}
}
},
"repeater_neighbours": "Grannar",
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
"repeater_neighbors": "Grannar",
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
"neighbors_receivedData": "Mottagna grannars data",
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
"neighbors_repeatersNeighbours": "Upprepar grannar",
"neighbors_repeatersNeighbors": "Upprepar grannar",
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
"channels_createPrivateChannel": "Skapa en privat kanal",
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
@@ -1561,6 +1566,8 @@
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
"appSettings_languageUk": "Ukrainska",
"appSettings_enableMessageTracing": "Aktivera meddelandespårning",
"appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden",
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
"contacts_contactImported": "Kontakt har importerats.",
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}",
"notification_newTypeDiscovered": "Ny {contactType} upptäckt",
"notification_receivedNewMessage": "Nytt meddelande mottaget",
"contacts_ShareContactZeroHop": "Dela kontakt via annons",
"settings_gpxExportAll": "Exportera alla kontakter till GPX",
"settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.",
"settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång",
@@ -1591,6 +1597,207 @@
"settings_gpxExportAllContacts": "Alla kontakters platser",
"settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata",
"settings_gpxExportShareText": "Kartdata exporterad från meshcore-open",
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!"
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!",
"pathTrace_clearTooltip": "Rensa väg",
"map_pathTraceCancelled": "Sökvägsspårning avbruten.",
"map_runTrace": "Kör spårsökning",
"map_tapToAdd": "Tryck på noder för att lägga till dem i banan.",
"map_removeLast": "Ta bort sista",
"scanner_enableBluetooth": "Aktivera Bluetooth",
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
"scanner_bluetoothOff": "Bluetooth är avstängt",
"snrIndicator_lastSeen": "Senast sedd",
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
"chat_ShowAllPaths": "Visa alla vägar",
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
"settings_clientRepeat": "Upprepa utan elnät",
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.",
"settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Enheter",
"appSettings_unitsMetric": "Metriskt (m/km)",
"appSettings_unitsImperial": "Imperialt (ft / mi)",
"map_lineOfSight": "Synlinje",
"map_losScreenTitle": "Synlinje",
"losSelectStartEnd": "Välj start- och slutnoder för LOS.",
"losRunFailed": "Synlinjekontroll misslyckades: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Rensa alla punkter",
"losRunToViewElevationProfile": "Kör LOS för att se höjdprofil",
"losMenuTitle": "LOS-menyn",
"losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter",
"losShowDisplayNodes": "Visa displaynoder",
"losCustomPoints": "Anpassade poäng",
"losCustomPointLabel": "Anpassad {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antenn A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenn B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Kör LOS",
"losNoElevationData": "Inga höjddata",
"losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: kollar...",
"losStatusNoData": "LOS: inga data",
"losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.",
"losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.",
"losRenameCustomPoint": "Byt namn på anpassad punkt",
"losPointName": "Punktnamn",
"losShowPanelTooltip": "Visa LOS-panelen",
"losHidePanelTooltip": "Dölj LOS-panelen",
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radiohorisont",
"losLegendLosBeam": "Siktlinje",
"losLegendTerrain": "Terräng",
"losFrequencyLabel": "Frekvens",
"losFrequencyInfoTooltip": "Visa detaljer om beräkningen",
"losFrequencyDialogTitle": "Beräkning av radiohorisonten",
"losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Ta bort från favoriter",
"listFilter_addToFavorites": "Lägg till i favoriter",
"listFilter_favorites": "Favoriter",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Oläst",
"contacts_searchContactsNoNumber": "Sök kontakter...",
"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..."
}
+218 -11
View File
@@ -1,4 +1,12 @@
{
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "uk",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакти",
@@ -131,9 +139,6 @@
"settings_infoContactsCount": "Кількість контактів",
"settings_infoChannelCount": "Кількість каналів",
"settings_presets": "Попередні налаштування",
"settings_preset915Mhz": "915 МГц",
"settings_preset868Mhz": "868 МГц",
"settings_preset433Mhz": "433 МГц",
"settings_frequency": "Частота (МГц)",
"settings_frequencyHelper": "300.0 - 2500.0",
"settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)",
@@ -143,8 +148,6 @@
"settings_txPower": "Потужність TX (дБм)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)",
"settings_longRange": "Дальній діапазон",
"settings_fastSpeed": "Висока швидкість",
"settings_error": "Помилка: {message}",
"@settings_error": {
"placeholders": {
@@ -340,6 +343,8 @@
"channels_publicChannel": "Публічний канал",
"channels_privateChannel": "Приватний канал",
"channels_editChannel": "Редагувати канал",
"channels_muteChannel": "Вимкнути сповіщення каналу",
"channels_unmuteChannel": "Увімкнути сповіщення каналу",
"channels_deleteChannel": "Видалити канал",
"channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.",
"@channels_deleteChannelConfirm": {
@@ -1357,12 +1362,12 @@
}
}
},
"repeater_neighbours": "Сусіди",
"repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.",
"repeater_neighbors": "Сусіди",
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
"neighbors_receivedData": "Дані сусідів отримано",
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
"neighbors_repeatersNeighbours": "Ретранслятори-сусіди",
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
"neighbors_noData": "Дані про сусідів недоступні.",
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
@@ -1562,6 +1567,8 @@
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
"contacts_clipboardEmpty": "Буфер обміну порожній",
"appSettings_languageRu": "Російська",
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
@@ -1575,7 +1582,6 @@
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}",
"notification_newTypeDiscovered": "Виявлено новий {contactType}",
"notification_receivedNewMessage": "Отримано нове повідомлення",
"contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням",
"settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX",
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.",
"settings_gpxExportSuccess": "Успішно експортовано файл GPX.",
@@ -1591,6 +1597,207 @@
"settings_gpxExportShareText": "Дані карти експортовані з meshcore-open",
"settings_gpxExportAllContacts": "Усі місця контактів",
"settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX",
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!"
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!",
"map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху",
"map_runTrace": "Виконати трасування шляху",
"pathTrace_clearTooltip": "Очистити шлях",
"map_removeLast": "Видалити останній",
"map_pathTraceCancelled": "Відмінується трасування шляху",
"scanner_enableBluetooth": "Увімкніть Bluetooth",
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
"scanner_bluetoothOff": "Bluetooth вимкнено",
"snrIndicator_lastSeen": "Останній раз бачили",
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
"chat_ShowAllPaths": "Показати всі шляхи",
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
"settings_clientRepeat": "Автономна система",
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "одиниці",
"appSettings_unitsMetric": "Метричний (м / км)",
"appSettings_unitsImperial": "Імперська (ft / mi)",
"map_lineOfSight": "Пряма видимість",
"map_losScreenTitle": "Пряма видимість",
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
"losRunFailed": "Помилка перевірки прямої видимості: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Очистити всі пункти",
"losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти",
"losMenuTitle": "Меню LOS",
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
"losShowDisplayNodes": "Показати вузли відображення",
"losCustomPoints": "Користувальницькі точки",
"losCustomPointLabel": "Спеціальний {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Точка А",
"losPointB": "Точка Б",
"losAntennaA": "Антена A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Антена B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Запустіть LOS",
"losNoElevationData": "Немає даних про висоту",
"losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: перевірка...",
"losStatusNoData": "LOS: немає даних",
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
"losRenameCustomPoint": "Перейменуйте спеціальну точку",
"losPointName": "Назва точки",
"losShowPanelTooltip": "Показати панель LOS",
"losHidePanelTooltip": "Приховати панель LOS",
"losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Радіогоризонт",
"losLegendLosBeam": "Лінія прямої видимості",
"losLegendTerrain": "Рельєф",
"losFrequencyLabel": "Частота",
"losFrequencyInfoTooltip": "Переглянути деталі розрахунку",
"losFrequencyDialogTitle": "Розрахунок радіогоризонту",
"losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Видалити зі списку улюблених",
"listFilter_addToFavorites": "Додати до улюблених",
"listFilter_favorites": "Улюблені",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...",
"contacts_searchUsers": "Пошук {number}{str} користувачів...",
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
"contacts_searchContactsNoNumber": "Пошук контактів...",
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
"contacts_unread": "Непрочитане"
}
+694 -482
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart';
import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart';
@@ -14,6 +15,7 @@ import 'services/ble_debug_log_service.dart';
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 'storage/prefs_manager.dart';
import 'utils/app_logger.dart';
@@ -33,6 +35,7 @@ void main() async {
final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
// Load settings
await appSettingsService.loadSettings();
@@ -47,6 +50,9 @@ void main() async {
final notificationService = NotificationService();
await notificationService.initialize();
await backgroundService.initialize();
_registerThirdPartyLicenses();
await chatTextScaleService.initialize();
// Wire up connector with services
connector.initialize(
@@ -76,10 +82,32 @@ void main() async {
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
),
);
}
void _registerThirdPartyLicenses() {
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
<String>['Open-Meteo Elevation API Data'],
'''
Data used by LOS elevation lookups is provided by Open-Meteo.
Open-Meteo terms and attribution:
https://open-meteo.com/en/terms
Elevation API:
https://open-meteo.com/en/docs/elevation-api
Attribution license reference:
Creative Commons Attribution 4.0 International (CC BY 4.0)
https://creativecommons.org/licenses/by/4.0/
''',
);
});
}
class MeshCoreApp extends StatelessWidget {
final MeshCoreConnector connector;
final MessageRetryService retryService;
@@ -89,6 +117,7 @@ class MeshCoreApp extends StatelessWidget {
final BleDebugLogService bleDebugLogService;
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
const MeshCoreApp({
super.key,
@@ -100,6 +129,7 @@ class MeshCoreApp extends StatelessWidget {
required this.bleDebugLogService,
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
});
@override
@@ -112,6 +142,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: appSettingsService),
ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),
],
+56 -1
View File
@@ -1,3 +1,16 @@
enum UnitSystem { metric, imperial }
extension UnitSystemValue on UnitSystem {
String get value {
switch (this) {
case UnitSystem.imperial:
return 'imperial';
case UnitSystem.metric:
return 'metric';
}
}
}
class AppSettings {
static const Object _unset = Object();
@@ -9,6 +22,7 @@ class AppSettings {
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
final bool mapShowMarkers;
final bool enableMessageTracing;
final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom;
final int mapCacheMaxZoom;
@@ -21,6 +35,9 @@ class AppSettings {
final String? languageOverride; // null = system default
final bool appDebugLogEnabled;
final Map<String, String> batteryChemistryByDeviceId;
final Map<String, String> batteryChemistryByRepeaterId;
final UnitSystem unitSystem;
final Set<String> mutedChannels;
AppSettings({
this.clearPathOnMaxRetry = false,
@@ -31,6 +48,7 @@ class AppSettings {
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
this.mapShowMarkers = true,
this.enableMessageTracing = false,
this.mapCacheBounds,
this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15,
@@ -43,7 +61,12 @@ class AppSettings {
this.languageOverride,
this.appDebugLogEnabled = false,
Map<String, String>? batteryChemistryByDeviceId,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
Map<String, String>? batteryChemistryByRepeaterId,
this.unitSystem = UnitSystem.metric,
Set<String>? mutedChannels,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {};
Map<String, dynamic> toJson() {
return {
@@ -55,6 +78,7 @@ class AppSettings {
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
'map_show_markers': mapShowMarkers,
'enable_message_tracing': enableMessageTracing,
'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom,
'map_cache_max_zoom': mapCacheMaxZoom,
@@ -67,10 +91,20 @@ class AppSettings {
'language_override': languageOverride,
'app_debug_log_enabled': appDebugLogEnabled,
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
'unit_system': unitSystem.value,
'muted_channels': mutedChannels.toList(),
};
}
factory AppSettings.fromJson(Map<String, dynamic> json) {
UnitSystem parseUnitSystem(dynamic value) {
if (value is String && value.toLowerCase() == 'imperial') {
return UnitSystem.imperial;
}
return UnitSystem.metric;
}
return AppSettings(
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
@@ -81,6 +115,7 @@ class AppSettings {
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
mapShowMarkers: json['map_show_markers'] 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()),
),
@@ -101,6 +136,17 @@ class AppSettings {
(key, value) => MapEntry(key.toString(), value.toString()),
) ??
{},
batteryChemistryByRepeaterId:
(json['battery_chemistry_by_repeater_id'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), value.toString()),
) ??
{},
unitSystem: parseUnitSystem(json['unit_system']),
mutedChannels:
((json['muted_channels'] as List?)
?.map((e) => e.toString())
.toSet()) ??
{},
);
}
@@ -113,6 +159,7 @@ class AppSettings {
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
bool? mapShowMarkers,
bool? enableMessageTracing,
Object? mapCacheBounds = _unset,
int? mapCacheMinZoom,
int? mapCacheMaxZoom,
@@ -125,6 +172,9 @@ class AppSettings {
Object? languageOverride = _unset,
bool? appDebugLogEnabled,
Map<String, String>? batteryChemistryByDeviceId,
Map<String, String>? batteryChemistryByRepeaterId,
UnitSystem? unitSystem,
Set<String>? mutedChannels,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
@@ -135,6 +185,7 @@ class AppSettings {
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds
: mapCacheBounds as Map<String, double>?,
@@ -154,6 +205,10 @@ class AppSettings {
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
batteryChemistryByDeviceId:
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
batteryChemistryByRepeaterId:
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
unitSystem: unitSystem ?? this.unitSystem,
mutedChannels: mutedChannels ?? this.mutedChannels,
);
}
}
+14 -3
View File
@@ -5,6 +5,7 @@ class Contact {
final Uint8List publicKey;
final String name;
final int type;
final int flags;
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
final Uint8List path; // Path bytes from device
final int?
@@ -19,6 +20,7 @@ class Contact {
required this.publicKey,
required this.name,
required this.type,
this.flags = 0,
required this.pathLength,
required this.path,
this.pathOverride,
@@ -58,11 +60,13 @@ class Contact {
}
bool get hasLocation => latitude != null && longitude != null;
bool get isFavorite => (flags & contactFlagFavorite) != 0;
Contact copyWith({
Uint8List? publicKey,
String? name,
int? type,
int? flags,
int? pathLength,
Uint8List? path,
int? pathOverride,
@@ -77,6 +81,7 @@ class Contact {
publicKey: publicKey ?? this.publicKey,
name: name ?? this.name,
type: type ?? this.type,
flags: flags ?? this.flags,
pathLength: pathLength ?? this.pathLength,
path: path ?? this.path,
pathOverride: clearPathOverride
@@ -119,7 +124,7 @@ class Contact {
final pathBytes = _pathBytesForDisplay;
Uint8List? traceBytes;
if (pathLength <= 0) {
if (pathBytes.isEmpty) {
traceBytes = Uint8List(1);
traceBytes[0] = publicKey[0];
return traceBytes;
@@ -160,13 +165,14 @@ class Contact {
}
static Contact? fromFrame(Uint8List data) {
if (data.length < contactFrameSize) return null;
if (data.isEmpty) return null;
if (data[0] != respCodeContact) return null;
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 safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
@@ -191,12 +197,17 @@ class Contact {
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
);
} catch (e) {
// If parsing fails, return null
return null;
}
}
@override
+183 -29
View File
@@ -59,46 +59,200 @@ class RadioSettings {
required this.txPowerDbm,
});
// Preset configurations
static RadioSettings get preset915MHz => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
// Regional preset configurations
static final List<(String, RadioSettings)> presets = [
(
'Australia',
RadioSettings(
frequencyMHz: 915.8,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Australia (Narrow)',
RadioSettings(
frequencyMHz: 916.575,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get preset868MHz => RadioSettings(
frequencyMHz: 868.0,
bandwidth: LoRaBandwidth.bw125,
),
),
(
'Australia SA, WA, QLD',
RadioSettings(
frequencyMHz: 923.125,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Czech Republic',
RadioSettings(
frequencyMHz: 869.432,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
);
static RadioSettings get preset433MHz => RadioSettings(
),
),
(
'EU 433MHz',
RadioSettings(
frequencyMHz: 433.650,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'EU/UK (Long Range)',
RadioSettings(
frequencyMHz: 869.525,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'EU/UK (Medium Range)',
RadioSettings(
frequencyMHz: 869.525,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'EU/UK (Narrow)',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'New Zealand',
RadioSettings(
frequencyMHz: 917.375,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'New Zealand (Narrow)',
RadioSettings(
frequencyMHz: 917.375,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Portugal 433',
RadioSettings(
frequencyMHz: 433.375,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf9,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Portugal 869',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'Switzerland',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'USA Arizona',
RadioSettings(
frequencyMHz: 908.205,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'USA/Canada',
RadioSettings(
frequencyMHz: 910.525,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Vietnam',
RadioSettings(
frequencyMHz: 920.250,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
// Off-grid repeat presets (valid client_repeat frequencies)
(
'Off-Grid 433',
RadioSettings(
frequencyMHz: 433.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get presetLongRange => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf12,
codingRate: LoRaCodingRate.cr4_8,
txPowerDbm: 20,
);
static RadioSettings get presetFastSpeed => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw500,
spreadingFactor: LoRaSpreadingFactor.sf7,
),
),
(
'Off-Grid 869',
RadioSettings(
frequencyMHz: 869.0,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'Off-Grid 918',
RadioSettings(
frequencyMHz: 918.0,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
),
),
];
int get frequencyHz => (frequencyMHz * 1000).round();
int get bandwidthHz => bandwidth.hz;
+3 -2
View File
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../services/app_debug_log_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
class AppDebugLogScreen extends StatelessWidget {
const AppDebugLogScreen({super.key});
@@ -17,7 +18,7 @@ class AppDebugLogScreen extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.debugLog_appTitle),
title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle),
centerTitle: true,
actions: [
IconButton(
@@ -55,7 +56,7 @@ class AppDebugLogScreen extends StatelessWidget {
child: hasEntries
? ListView.separated(
itemCount: entries.length,
separatorBuilder: (_, __) => const Divider(height: 1),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final entry = entries[index];
return ListTile(
+84 -3
View File
@@ -3,8 +3,10 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../services/app_settings_service.dart';
import '../services/notification_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'map_cache_screen.dart';
class AppSettingsScreen extends StatelessWidget {
@@ -14,7 +16,7 @@ class AppSettingsScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.appSettings_title),
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
centerTitle: true,
),
body: SafeArea(
@@ -80,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget {
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(context, settingsService),
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.location_searching),
title: Text(context.l10n.appSettings_enableMessageTracing),
subtitle: Text(
context.l10n.appSettings_enableMessageTracingSubtitle,
),
value: settingsService.settings.enableMessageTracing,
onChanged: (value) {
settingsService.setEnableMessageTracing(value);
},
),
],
),
);
@@ -360,6 +374,18 @@ class AppSettingsScreen extends StatelessWidget {
onTap: () => _showTimeFilterDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.straighten),
title: Text(context.l10n.appSettings_unitsTitle),
subtitle: Text(
settingsService.settings.unitSystem == UnitSystem.imperial
? context.l10n.appSettings_unitsImperial
: context.l10n.appSettings_unitsMetric,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showUnitsDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.download_outlined),
title: Text(context.l10n.appSettings_offlineMapCache),
@@ -384,6 +410,7 @@ class AppSettingsScreen extends StatelessWidget {
);
}
// Fixed rendering issues
Widget _buildBatteryCard(
BuildContext context,
AppSettingsService settingsService,
@@ -399,6 +426,7 @@ class AppSettingsScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
@@ -406,6 +434,8 @@ class AppSettingsScreen extends StatelessWidget {
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
// Main tile (icon + text only)
ListTile(
leading: const Icon(Icons.battery_full),
title: Text(context.l10n.appSettings_batteryChemistry),
@@ -416,8 +446,19 @@ class AppSettingsScreen extends StatelessWidget {
)
: context.l10n.appSettings_batteryChemistryConnectFirst,
),
trailing: DropdownButton<String>(
value: selection,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
// Dropdown (separate full-width row)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: DropdownButtonFormField<String>(
initialValue: selection,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
),
onChanged: isConnected
? (value) {
if (value != null) {
@@ -691,6 +732,46 @@ class AppSettingsScreen extends StatelessWidget {
);
}
void _showUnitsDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.appSettings_unitsTitle),
content: RadioGroup<UnitSystem>(
groupValue: settingsService.settings.unitSystem,
onChanged: (value) {
if (value != null) {
settingsService.setUnitSystem(value);
Navigator.pop(context);
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(context.l10n.appSettings_unitsMetric),
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
),
ListTile(
title: Text(context.l10n.appSettings_unitsImperial),
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
),
);
}
Widget _buildDebugCard(
BuildContext context,
AppSettingsService settingsService,
+3 -2
View File
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import '../l10n/l10n.dart';
import '../services/ble_debug_log_service.dart';
import '../connector/meshcore_protocol.dart';
import '../widgets/adaptive_app_bar_title.dart';
enum _BleLogView { frames, rawLogRx }
@@ -29,7 +30,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
: rawEntries.isNotEmpty;
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.debugLog_bleTitle),
title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle),
actions: [
IconButton(
tooltip: context.l10n.debugLog_copyLog,
@@ -100,7 +101,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
itemCount: showingFrames
? entries.length
: rawEntries.length,
separatorBuilder: (_, __) => const Divider(height: 1),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
if (showingFrames) {
final entry = entries[index];
+380 -38
View File
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -16,11 +17,15 @@ import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
import '../models/channel_message.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../utils/emoji_utils.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_status_icon.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -216,7 +221,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Stack(
children: [
ListView.builder(
ChatZoomWrapper(
child: ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.all(8),
@@ -244,10 +250,22 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
return Container(
key: _messageKeys[message.messageId]!,
child: _buildMessageBubble(message),
child: Builder(
builder: (context) {
final textScale = context
.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _buildMessageBubble(
message,
textScale,
);
},
),
);
},
),
),
JumpToBottomButton(scrollController: _scrollController),
],
);
@@ -261,7 +279,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
Widget _buildMessageBubble(ChannelMessage message) {
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing;
final gifId = _parseGifId(message.text);
final poi = _parsePoiMessage(message.text);
@@ -271,9 +291,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
? message.pathVariants.first
: Uint8List(0));
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column(
const maxSwipeOffset = 64.0;
const replySwipeThreshold = 64.0;
const bodyFontSize = 14.0;
final messageBody = Column(
crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
@@ -295,19 +316,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
@@ -334,12 +350,33 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (gifId == null) const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message),
_buildReplyPreview(message, textScale),
const SizedBox(height: 8),
],
if (poi != null)
_buildPoiMessage(context, poi, isOutgoing)
_buildPoiMessage(
context,
poi,
isOutgoing,
textScale,
trailing: (!enableTracing && isOutgoing)
? Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
)
: null,
)
else if (gifId != null)
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GifMessage(
@@ -354,13 +391,52 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.6),
),
),
if (!enableTracing && isOutgoing)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(
context,
).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(10),
topRight: Radius.circular(8),
),
),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
),
),
],
)
else
Linkify(
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Linkify(
text: message.text,
style: const TextStyle(fontSize: 14),
linkStyle: const TextStyle(
fontSize: 14,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
fontSize: bodyFontSize * textScale,
color: Colors.green,
decoration: TextDecoration.underline,
),
@@ -369,9 +445,30 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) =>
LinkHandler.handleLinkTap(context, link.url),
onOpen: (link) => LinkHandler.handleLinkTap(
context,
link.url,
),
),
),
if (!enableTracing && isOutgoing) ...[
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
),
],
],
),
if (enableTracing) ...[
if (displayPath.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
@@ -443,6 +540,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
),
],
],
),
),
),
@@ -457,11 +555,66 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
],
],
);
if (!isOutgoing) {
return _SwipeReplyBubble(
maxSwipeOffset: maxSwipeOffset,
replySwipeThreshold: replySwipeThreshold,
onReplyTriggered: () => _setReplyingTo(message),
hintBuilder: ({required isStart}) =>
_buildReplySwipeHint(isStart: isStart),
child: messageBody,
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: messageBody,
);
}
}
Widget _buildReplySwipeHint({required bool isStart}) {
final colorScheme = Theme.of(context).colorScheme;
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.reply, color: colorScheme.primary),
const SizedBox(width: 6),
Text(
context.l10n.chat_reply,
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
);
return Container(
alignment: isStart ? Alignment.centerLeft : Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 16),
color: colorScheme.primary.withValues(alpha: 0.08),
child: isStart
? content
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.chat_reply,
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 6),
Icon(Icons.reply, color: colorScheme.primary),
],
),
);
}
Widget _buildReplyPreview(ChannelMessage message) {
Widget _buildReplyPreview(ChannelMessage message, double textScale) {
final connector = context.read<MeshCoreConnector>();
final isOwnNode = message.replyToSenderName == connector.selfName;
final replyText = message.replyToText ?? '';
@@ -489,7 +642,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const SizedBox(width: 4),
Text(
context.l10n.chat_location,
style: TextStyle(fontSize: 12, color: previewTextColor),
style: TextStyle(fontSize: 12 * textScale, color: previewTextColor),
),
],
);
@@ -499,7 +652,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
fontSize: 12 * textScale,
color: previewTextColor,
fontStyle: FontStyle.italic,
),
@@ -523,7 +676,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text(
context.l10n.chat_replyTo(message.replyToSenderName ?? ''),
style: TextStyle(
fontSize: 11,
fontSize: 11 * textScale,
fontWeight: FontWeight.bold,
color: isOwnNode
? Theme.of(context).colorScheme.primary
@@ -599,7 +752,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return _PoiInfo(lat: lat, lon: lon, label: label);
}
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
Widget _buildPoiMessage(
BuildContext context,
_PoiInfo poi,
bool isOutgoing,
double textScale, {
Widget? trailing,
}) {
final colorScheme = Theme.of(context).colorScheme;
final textColor = isOutgoing
? colorScheme.onPrimaryContainer
@@ -635,16 +794,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
children: [
Text(
context.l10n.chat_poiShared,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14 * textScale,
),
),
if (poi.label.isNotEmpty)
Text(
poi.label,
style: TextStyle(color: metaColor, fontSize: 12),
style: TextStyle(color: metaColor, fontSize: 12 * textScale),
),
],
),
),
if (trailing != null) ...[const SizedBox(width: 4), trailing],
],
);
}
@@ -709,7 +873,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return colors[hash.abs() % colors.length];
}
Widget _buildReplyBanner() {
Widget _buildReplyBanner(double textScale) {
final message = _replyingToMessage!;
return Container(
width: double.infinity,
@@ -735,7 +899,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text(
context.l10n.chat_replyingTo(message.senderName),
style: TextStyle(
fontSize: 12,
fontSize: 12 * textScale,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
@@ -745,7 +909,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontSize: 11 * textScale,
color: Theme.of(
context,
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
@@ -772,7 +936,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_replyingToMessage != null) _buildReplyBanner(),
if (_replyingToMessage != null)
Builder(
builder: (context) {
final textScale = context.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _buildReplyBanner(textScale);
},
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
@@ -798,7 +970,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
builder: (context, value, child) {
final gifId = _parseGifId(value.text);
if (gifId != null) {
return Row(
return Focus(
autofocus: true,
onKeyEvent: (node, event) {
if (event is KeyDownEvent &&
(event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey ==
LogicalKeyboardKey.numpadEnter)) {
_sendMessage();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: Row(
children: [
Expanded(
child: ClipRRect(
@@ -809,9 +993,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
fallbackTextColor: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
fallbackTextColor: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
maxSize: 160,
),
),
@@ -819,9 +1004,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _textController.clear(),
onPressed: () {
_textController.clear();
_textFieldFocusNode.requestFocus();
},
),
],
),
);
}
@@ -884,6 +1073,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
connector.sendChannelMessage(widget.channel, messageText);
_textController.clear();
_cancelReply();
_textFieldFocusNode.requestFocus();
}
String _formatTime(DateTime time) {
@@ -901,7 +1091,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelMessagePathScreen(message: message),
builder: (context) =>
ChannelMessagePathScreen(message: message, channelMessage: true),
),
);
}
@@ -1006,6 +1197,157 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
}
class _SwipeReplyBubble extends StatefulWidget {
final double maxSwipeOffset;
final double replySwipeThreshold;
final VoidCallback onReplyTriggered;
final Widget Function({required bool isStart}) hintBuilder;
final Widget child;
const _SwipeReplyBubble({
required this.maxSwipeOffset,
required this.replySwipeThreshold,
required this.onReplyTriggered,
required this.hintBuilder,
required this.child,
});
@override
State<_SwipeReplyBubble> createState() => _SwipeReplyBubbleState();
}
class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
Offset? _swipeStartPosition;
double _swipeOffset = 0;
double _maxSwipeDistance = 0;
int? _swipePointerId;
bool _swipeLockedToHorizontal = false;
void _handleSwipeStart(Offset position) {
_swipeStartPosition = position;
_maxSwipeDistance = 0;
if (_swipeOffset != 0) {
setState(() => _swipeOffset = 0);
}
}
void _handleSwipePointerDown(PointerDownEvent event) {
_swipePointerId = event.pointer;
_swipeLockedToHorizontal = false;
_handleSwipeStart(event.position);
}
void _handleSwipePointerMove(PointerMoveEvent event) {
if (_swipePointerId != event.pointer || _swipeStartPosition == null) {
return;
}
final dx = event.position.dx - _swipeStartPosition!.dx;
const axisLockThreshold = 12.0;
if (!_swipeLockedToHorizontal) {
if (-dx < axisLockThreshold) {
return;
}
_swipeLockedToHorizontal = true;
}
_handleSwipeUpdate(event.position);
}
void _handleSwipeUpdate(Offset position) {
if (_swipeStartPosition == null) return;
final dx = position.dx - _swipeStartPosition!.dx;
if (dx >= 0) return;
if (-dx < 6) return;
if (-dx > _maxSwipeDistance) {
_maxSwipeDistance = -dx;
}
final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble();
final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
if (adjusted != _swipeOffset) {
setState(() => _swipeOffset = adjusted);
}
}
void _handleSwipePointerUp(Offset position) {
if (_swipeLockedToHorizontal && _swipeStartPosition != null) {
final dx = position.dx - _swipeStartPosition!.dx;
final peak = math.max(
_maxSwipeDistance,
(-dx).clamp(0.0, double.infinity),
);
if (peak >= widget.replySwipeThreshold) {
widget.onReplyTriggered();
HapticFeedback.selectionClick();
}
}
_resetSwipe();
}
void _resetSwipe() {
if (_swipeOffset != 0) {
setState(() => _swipeOffset = 0);
}
_swipeStartPosition = null;
_maxSwipeDistance = 0;
_swipePointerId = null;
_swipeLockedToHorizontal = false;
}
double _applySwipeResistance(double rawOffset, double maxOffset) {
final abs = rawOffset.abs();
if (abs <= 0) return 0;
final norm = (abs / maxOffset).clamp(0.0, 1.0);
const deadZone = 0.18;
if (norm <= deadZone) {
return rawOffset.sign * maxOffset * (norm * 0.08);
}
final t = ((norm - deadZone) / (1 - deadZone)).clamp(0.0, 1.0);
final curved = t < 0.5
? 16 * math.pow(t, 5)
: 1 - math.pow(-2 * t + 2, 5) / 2;
const deadZoneEnd = 0.0144;
return rawOffset.sign *
maxOffset *
(deadZoneEnd + curved * (1 - deadZoneEnd));
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _handleSwipePointerDown,
onPointerMove: _handleSwipePointerMove,
onPointerUp: (event) => _handleSwipePointerUp(event.position),
onPointerCancel: (_) => _resetSwipe(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Opacity(
opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
child: widget.hintBuilder(isStart: false),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 150),
transform: Matrix4.translationValues(_swipeOffset, 0, 0),
curve: Curves.easeOut,
child: widget.child,
),
],
),
),
);
}
}
class _PoiInfo {
final double lat;
final double lon;
+161 -29
View File
@@ -9,26 +9,38 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../services/map_tile_cache_service.dart';
import '../services/app_settings_service.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/app_localizations.dart';
import '../l10n/l10n.dart';
import '../models/channel_message.dart';
import '../models/app_settings.dart';
import '../models/contact.dart';
import '../widgets/adaptive_app_bar_title.dart';
class ChannelMessagePathScreen extends StatelessWidget {
final ChannelMessage message;
const ChannelMessagePathScreen({super.key, required this.message});
final bool channelMessage;
const ChannelMessagePathScreen({
super.key,
required this.message,
this.channelMessage = false,
});
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final l10n = context.l10n;
final primaryPath = _selectPrimaryPath(
final primaryPathTmp = _selectPrimaryPath(
message.pathBytes,
message.pathVariants,
);
final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp;
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops(
@@ -37,10 +49,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
l10n,
);
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
return Scaffold(
appBar: AppBar(
title: Text(l10n.channelPath_title),
title: AdaptiveAppBarTitle(l10n.channelPath_title),
actions: [
IconButton(
icon: const Icon(Icons.radar_outlined),
@@ -50,9 +61,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(primaryPath),
path: primaryPath,
flipPathRound: true,
reversePathRound: true,
reversePathRound: !message.isOutgoing && !channelMessage,
),
),
),
@@ -62,7 +73,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
tooltip: l10n.channelPath_viewMap,
onPressed: hasHopDetails
? () {
_openPathMap(context);
_openPathMap(context, channelMessage: channelMessage);
}
: null,
),
@@ -157,7 +168,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
),
subtitle: Text(_formatPathPrefixes(variants[i])),
trailing: const Icon(Icons.map_outlined, size: 20),
onTap: () => _openPathMap(context, initialPath: variants[i]),
onTap: () => _openPathMap(
context,
initialPath: variants[i],
channelMessage: channelMessage,
),
),
),
],
@@ -248,13 +263,18 @@ class ChannelMessagePathScreen extends StatelessWidget {
);
}
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
void _openPathMap(
BuildContext context, {
Uint8List? initialPath,
bool channelMessage = false,
}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelMessagePathMapScreen(
message: message,
initialPath: initialPath,
channelMessage: channelMessage,
),
),
);
@@ -264,11 +284,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
class ChannelMessagePathMapScreen extends StatefulWidget {
final ChannelMessage message;
final Uint8List? initialPath;
final bool channelMessage;
const ChannelMessagePathMapScreen({
super.key,
required this.message,
this.initialPath,
this.channelMessage = false,
});
@override
@@ -278,8 +300,12 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
class _ChannelMessagePathMapScreenState
extends State<ChannelMessagePathMapScreen> {
static const double _labelZoomThreshold = 8.5;
Uint8List? _selectedPath;
double _pathDistance = 0.0;
bool _showNodeLabels = true;
bool _didReceivePositionUpdate = false;
@override
void initState() {
@@ -314,6 +340,8 @@ class _ChannelMessagePathMapScreenState
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final primaryPath = _selectPrimaryPath(
widget.message.pathBytes,
@@ -323,11 +351,18 @@ class _ChannelMessagePathMapScreenState
primaryPath,
widget.message.pathVariants,
);
final selectedPath = _resolveSelectedPath(
final selectedPathTmp = _resolveSelectedPath(
_selectedPath,
observedPaths,
primaryPath,
);
final selectedPath =
((!widget.message.isOutgoing && !widget.channelMessage) ||
(widget.message.isOutgoing && widget.channelMessage))
? Uint8List.fromList(selectedPathTmp.reversed.toList())
: selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths);
final hops = _buildPathHops(
selectedPath,
@@ -336,12 +371,22 @@ class _ChannelMessagePathMapScreenState
);
final points = <LatLng>[];
if ((widget.message.isOutgoing && !widget.channelMessage) ||
(widget.message.isOutgoing && widget.channelMessage)) {
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
}
for (final hop in hops) {
if (hop.hasLocation) {
points.add(hop.position!);
}
}
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
(!widget.message.isOutgoing && widget.channelMessage)) {
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
}
final polylines = points.length > 1
? [
@@ -357,6 +402,9 @@ class _ChannelMessagePathMapScreenState
? points.first
: const LatLng(0, 0);
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
if (!_didReceivePositionUpdate) {
_showNodeLabels = initialZoom >= _labelZoomThreshold;
}
final bounds = points.length > 1
? LatLngBounds.fromPoints(points)
: null;
@@ -366,7 +414,9 @@ class _ChannelMessagePathMapScreenState
_pathDistance = _getPathDistance(points);
return Scaffold(
appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)),
appBar: AppBar(
title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle),
),
body: SafeArea(
top: false,
child: Stack(
@@ -388,6 +438,17 @@ class _ChannelMessagePathMapScreenState
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate,
),
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (!_didReceivePositionUpdate ||
shouldShow != _showNodeLabels) {
if (!mounted) return;
setState(() {
_didReceivePositionUpdate = true;
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
@@ -399,7 +460,12 @@ class _ChannelMessagePathMapScreenState
),
if (polylines.isNotEmpty)
PolylineLayer(polylines: polylines),
MarkerLayer(markers: _buildHopMarkers(hops)),
MarkerLayer(
markers: _buildHopMarkers(
hops,
showLabels: _showNodeLabels,
),
),
],
),
if (observedPaths.length > 1)
@@ -422,7 +488,7 @@ class _ChannelMessagePathMapScreenState
),
),
),
_buildLegendCard(context, hops),
_buildLegendCard(context, hops, isImperial),
],
),
),
@@ -494,12 +560,17 @@ class _ChannelMessagePathMapScreenState
);
}
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
return [
for (final hop in hops)
if (hop.hasLocation)
List<Marker> _buildHopMarkers(
List<_PathHop> hops, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final hop in hops) {
if (!hop.hasLocation) continue;
final point = hop.position!;
markers.add(
Marker(
point: hop.position!,
point: point,
width: 35,
height: 35,
child: Container(
@@ -526,13 +597,24 @@ class _ChannelMessagePathMapScreenState
),
),
),
if (context.read<MeshCoreConnector>().selfLatitude != null &&
context.read<MeshCoreConnector>().selfLongitude != null)
Marker(
point: LatLng(
context.read<MeshCoreConnector>().selfLatitude!,
context.read<MeshCoreConnector>().selfLongitude!,
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: point,
label: hop.contact?.name ?? _formatPrefix(hop.prefix),
),
);
}
}
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
if (selfLat != null && selfLon != null) {
final selfPoint = LatLng(selfLat, selfLon);
markers.add(
Marker(
point: selfPoint,
width: 35,
height: 35,
child: Container(
@@ -559,10 +641,60 @@ class _ChannelMessagePathMapScreenState
),
),
),
];
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
return markers;
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
Widget _buildLegendCard(
BuildContext context,
List<_PathHop> hops,
bool isImperial,
) {
final l10n = context.l10n;
final maxHeight = MediaQuery.of(context).size.height * 0.35;
final estimatedHeight = 72.0 + (hops.length * 56.0);
@@ -581,7 +713,7 @@ class _ChannelMessagePathMapScreenState
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
@@ -594,7 +726,7 @@ class _ChannelMessagePathMapScreenState
: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: hops.length,
separatorBuilder: (_, __) => const Divider(height: 1),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final hop = hops[index];
return ListTile(
+90 -17
View File
@@ -3,18 +3,20 @@ import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/storage/channel_message_store.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../models/channel.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/battery_indicator.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
import '../widgets/qr_code_display.dart';
@@ -104,6 +106,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final channelMessageStore = ChannelMessageStore();
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
@@ -116,8 +119,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
leading: BatteryIndicator(connector: connector),
title: Text(context.l10n.channels_title),
title: AppBarTitle(context.l10n.channels_title),
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
@@ -304,6 +306,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
showDragHandle: true,
dragIndex: index,
@@ -323,6 +326,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
);
},
@@ -352,6 +356,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Widget _buildChannelTile(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel, {
bool showDragHandle = false,
int? dragIndex,
@@ -468,7 +473,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
);
}
},
onLongPress: () => _showChannelActions(context, connector, channel),
onLongPress: () => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
),
),
);
}
@@ -476,11 +486,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _showChannelActions(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel,
) {
final parentContext = context;
final settingsService = context.read<AppSettingsService>();
final isMuted = settingsService.isChannelMuted(channel.name);
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
context: parentContext,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -488,10 +503,30 @@ class _ChannelsScreenState extends State<ChannelsScreen>
leading: const Icon(Icons.edit_outlined),
title: Text(context.l10n.channels_editChannel),
onTap: () async {
Navigator.pop(context);
Navigator.pop(sheetContext);
await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) {
_showEditChannelDialog(context, connector, channel);
if (parentContext.mounted) {
_showEditChannelDialog(parentContext, connector, channel);
}
},
),
ListTile(
leading: Icon(
isMuted
? Icons.notifications_outlined
: Icons.notifications_off_outlined,
),
title: Text(
isMuted
? context.l10n.channels_unmuteChannel
: context.l10n.channels_muteChannel,
),
onTap: () async {
Navigator.pop(sheetContext);
if (isMuted) {
await settingsService.unmuteChannel(channel.name);
} else {
await settingsService.muteChannel(channel.name);
}
},
),
@@ -502,10 +537,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
style: const TextStyle(color: Colors.red),
),
onTap: () async {
Navigator.pop(context);
Navigator.pop(sheetContext);
await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) {
_confirmDeleteChannel(context, connector, channel);
if (parentContext.mounted) {
_confirmDeleteChannel(
context,
connector,
channelMessageStore,
channel,
);
}
},
),
@@ -1415,7 +1455,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: Text(dialogContext.l10n.common_cancel),
),
FilledButton(
onPressed: () {
onPressed: () async {
final name = nameController.text.trim();
final pskHex = pskController.text.trim();
@@ -1432,13 +1472,25 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
Navigator.pop(dialogContext);
connector.setChannel(channel.index, name, psk);
connector.setChannelSmazEnabled(channel.index, smazEnabled);
try {
await connector.setChannel(channel.index, name, psk);
await connector.setChannelSmazEnabled(
channel.index,
smazEnabled,
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.channels_channelUpdated(name)),
),
);
} catch (e, st) {
debugPrint(st.toString());
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update channel: $e')),
);
}
},
child: Text(dialogContext.l10n.common_save),
),
@@ -1451,6 +1503,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _confirmDeleteChannel(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel,
) {
showDialog(
@@ -1466,9 +1519,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: Text(dialogContext.l10n.common_cancel),
),
TextButton(
onPressed: () {
onPressed: () async {
Navigator.pop(dialogContext);
connector.deleteChannel(channel.index);
try {
await connector.deleteChannel(channel.index);
channelMessageStore.clearChannelMessages(channel.index);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
@@ -1476,6 +1535,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
),
);
} catch (e, st) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleteFailed(channel.name),
),
),
);
// Preserve existing logging (if it was there)
debugPrint('Failed to delete channel: $e\n$st');
}
},
child: Text(
dialogContext.l10n.common_delete,
+220 -22
View File
@@ -13,13 +13,19 @@ import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart';
import '../helpers/link_handler.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_history.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/path_history_service.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/elements_ui.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
import '../utils/emoji_utils.dart';
@@ -266,7 +272,8 @@ class _ChatScreenState extends State<ChatScreen> {
_scrollController.scrollToBottomIfAtBottom();
});
return ListView.builder(
return ChatZoomWrapper(
child: ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
@@ -302,17 +309,26 @@ class _ChatScreenState extends State<ChatScreen> {
.toUpperCase();
}
return Builder(
builder: (context) {
final textScale = context.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _MessageBubble(
message: message,
senderName: widget.contact.type == advTypeRoom
? "${contact.name} [$fourByteHex]"
: contact.name,
isRoomServer: widget.contact.type == advTypeRoom,
textScale: textScale,
onTap: () => _openMessagePath(message, contact),
onLongPress: () => _showMessageActions(message, contact),
);
},
);
},
),
);
}
Widget _buildInputBar(MeshCoreConnector connector) {
@@ -338,7 +354,19 @@ class _ChatScreenState extends State<ChatScreen> {
builder: (context, value, child) {
final gifId = _parseGifId(value.text);
if (gifId != null) {
return Row(
return Focus(
autofocus: true,
onKeyEvent: (node, event) {
if (event is KeyDownEvent &&
(event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey ==
LogicalKeyboardKey.numpadEnter)) {
_sendMessage(connector);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: Row(
children: [
Expanded(
child: ClipRRect(
@@ -357,9 +385,13 @@ class _ChatScreenState extends State<ChatScreen> {
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _textController.clear(),
onPressed: () {
_textController.clear();
_textFieldFocusNode.requestFocus();
},
),
],
),
);
}
@@ -427,16 +459,74 @@ class _ChatScreenState extends State<ChatScreen> {
connector.sendMessage(widget.contact, text);
_textController.clear();
_textFieldFocusNode.requestFocus();
}
void _showPathHistory(BuildContext context) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
bool showAllPaths = false;
showDialog(
context: context,
builder: (context) => Consumer<PathHistoryService>(
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => Consumer<PathHistoryService>(
builder: (context, pathService, _) {
final paths = pathService.getRecentPaths(widget.contact.publicKeyHex);
final paths = pathService.getRecentPaths(
widget.contact.publicKeyHex,
);
final repeatersList = List.of(connector.directRepeaters)
..sort((a, b) => b.ranking.compareTo(a.ranking));
if (repeatersList.isEmpty) {
showAllPaths = true;
}
final directRepeater = repeatersList.isEmpty
? null
: repeatersList.first;
final secondDirectRepeater = repeatersList.length < 2
? null
: repeatersList.elementAt(1);
final thirdDirectRepeater = repeatersList.length < 3
? null
: repeatersList.elementAt(2);
List<MapEntry<int, MapEntry<Color, PathRecord>>>
pathsWithRepeaters = paths.map((path) {
final isDirectRepeater =
directRepeater != null &&
path.pathBytes.isNotEmpty &&
directRepeater.pubkeyFirstByte == path.pathBytes.first;
final isSecondDirectRepeater =
secondDirectRepeater != null &&
path.pathBytes.isNotEmpty &&
secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
final isThirdDirectRepeater =
thirdDirectRepeater != null &&
path.pathBytes.isNotEmpty &&
thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
int ranking = -1;
Color color = Colors.grey;
if (isDirectRepeater) {
color = Colors.green;
ranking = 3;
} else if (isSecondDirectRepeater) {
color = Colors.yellow;
ranking = 2;
} else if (isThirdDirectRepeater) {
color = Colors.red;
ranking = 1;
} else if (path.wasFloodDiscovery) {
color = Colors.blue;
ranking = 0;
}
return MapEntry(ranking, MapEntry(color, path));
}).toList();
pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key));
return AlertDialog(
title: Row(
children: [
@@ -450,7 +540,18 @@ class _ChatScreenState extends State<ChatScreen> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (paths.isNotEmpty) ...[
if (pathsWithRepeaters.isNotEmpty) ...[
if (repeatersList.isNotEmpty)
FeatureToggleRow(
title: context.l10n.chat_ShowAllPaths,
subtitle: "",
value: showAllPaths,
onChanged: (val) {
setDialogState(() {
showAllPaths = val;
});
},
),
Text(
context.l10n.chat_recentAckPaths,
style: const TextStyle(
@@ -458,7 +559,7 @@ class _ChatScreenState extends State<ChatScreen> {
fontSize: 12,
),
),
if (paths.length >= 100) ...[
if (pathsWithRepeaters.length >= 100) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
@@ -477,16 +578,19 @@ class _ChatScreenState extends State<ChatScreen> {
),
],
const SizedBox(height: 8),
...paths.map((path) {
...pathsWithRepeaters.map((entry) {
final path = entry.value.value;
final color = entry.value.key;
if (!showAllPaths && entry.key < 1) {
return const SizedBox.shrink();
} else {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
dense: true,
leading: CircleAvatar(
radius: 16,
backgroundColor: path.wasFloodDiscovery
? Colors.blue
: Colors.green,
backgroundColor: color,
child: Text(
'${path.hopCount}',
style: const TextStyle(fontSize: 12),
@@ -533,7 +637,9 @@ class _ChatScreenState extends State<ChatScreen> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.chat_pathDetailsNotAvailable,
context
.l10n
.chat_pathDetailsNotAvailable,
),
duration: const Duration(seconds: 2),
),
@@ -564,6 +670,7 @@ class _ChatScreenState extends State<ChatScreen> {
},
),
);
}
}),
const Divider(),
] else ...[
@@ -668,6 +775,7 @@ class _ChatScreenState extends State<ChatScreen> {
);
},
),
),
);
}
@@ -1084,17 +1192,21 @@ class _MessageBubble extends StatelessWidget {
final bool isRoomServer;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final double textScale;
const _MessageBubble({
required this.message,
required this.senderName,
required this.isRoomServer,
required this.textScale,
this.onTap,
this.onLongPress,
});
@override
Widget build(BuildContext context) {
final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing;
final colorScheme = Theme.of(context).colorScheme;
final gifId = _parseGifId(message.text);
@@ -1109,6 +1221,7 @@ class _MessageBubble extends StatelessWidget {
? colorScheme.onErrorContainer
: (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface);
final metaColor = textColor.withValues(alpha: 0.7);
const bodyFontSize = 14.0;
String messageText = message.text;
if (isRoomServer && !isOutgoing) {
messageText = message.text.substring(4.clamp(0, message.text.length));
@@ -1172,8 +1285,30 @@ class _MessageBubble extends StatelessWidget {
if (gifId == null) const SizedBox(height: 4),
],
if (poi != null)
_buildPoiMessage(context, poi, textColor, metaColor)
_buildPoiMessage(
context,
poi,
textColor,
metaColor,
textScale,
trailing: (!enableTracing && isOutgoing)
? Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
MessageStatus.delivered &&
message.pathBytes.isNotEmpty,
isFailed:
message.status ==
MessageStatus.failed,
),
)
: null,
)
else if (gifId != null)
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
@@ -1184,23 +1319,78 @@ class _MessageBubble extends StatelessWidget {
alpha: 0.7,
),
),
),
if (!enableTracing && isOutgoing)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: bubbleColor,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(10),
topRight: Radius.circular(12),
),
),
child: MessageStatusIcon(
isAcked:
message.status ==
MessageStatus.delivered &&
message.pathBytes.isNotEmpty,
isFailed:
message.status ==
MessageStatus.failed,
),
),
),
],
)
else
Linkify(
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Linkify(
text: messageText,
style: TextStyle(color: textColor),
linkStyle: const TextStyle(
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),
onOpen: (link) => LinkHandler.handleLinkTap(
context,
link.url,
),
),
),
if (!enableTracing && isOutgoing) ...[
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
MessageStatus.delivered &&
message.pathBytes.isNotEmpty,
isFailed:
message.status == MessageStatus.failed,
),
),
],
],
),
if (enableTracing) ...[
if (isOutgoing && message.retryCount > 0) ...[
const SizedBox(height: 4),
Padding(
@@ -1269,6 +1459,7 @@ class _MessageBubble extends StatelessWidget {
),
),
],
],
),
),
),
@@ -1311,7 +1502,9 @@ class _MessageBubble extends StatelessWidget {
_PoiInfo poi,
Color textColor,
Color metaColor,
) {
double textScale, {
Widget? trailing,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -1338,16 +1531,21 @@ class _MessageBubble extends StatelessWidget {
children: [
Text(
context.l10n.chat_poiShared,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14 * textScale,
),
),
if (poi.label.isNotEmpty)
Text(
poi.label,
style: TextStyle(color: metaColor, fontSize: 12),
style: TextStyle(color: metaColor, fontSize: 12 * textScale),
),
],
),
),
if (trailing != null) ...[const SizedBox(width: 4), trailing],
],
);
}
+2 -1
View File
@@ -6,6 +6,7 @@ import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/qr_scanner_widget.dart';
/// Screen for scanning community QR codes to join communities.
@@ -29,7 +30,7 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.community_scanQr),
title: AdaptiveAppBarTitle(context.l10n.community_scanQr),
centerTitle: true,
),
body: _isProcessing
+99 -13
View File
@@ -3,6 +3,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
@@ -16,7 +18,6 @@ import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/emoji_utils.dart';
import '../utils/route_transitions.dart';
import '../widgets/battery_indicator.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
import '../widgets/quick_switch_bar.dart';
@@ -90,6 +91,7 @@ class _ContactsScreenState extends State<ContactsScreen>
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final frameBuffer = BufferReader(frame);
try {
final code = frameBuffer.readUInt8();
if (code == respCodeExportContact) {
@@ -130,7 +132,9 @@ class _ContactsScreenState extends State<ContactsScreen>
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
SnackBar(
content: Text(context.l10n.contacts_contactAdvertCopied),
),
);
}
@@ -143,7 +147,9 @@ class _ContactsScreenState extends State<ContactsScreen>
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
SnackBar(
content: Text(context.l10n.contacts_contactImportFailed),
),
);
}
@@ -164,6 +170,12 @@ class _ContactsScreenState extends State<ContactsScreen>
_pendingOperations.clear();
}
} catch (e) {
appLogger.error(
'Error processing received frame: $e',
tag: 'ContactsScreen',
);
}
});
}
@@ -171,14 +183,17 @@ class _ContactsScreenState extends State<ContactsScreen>
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactFrame = buildExportContactFrame(pubKey);
_pendingOperations.add(ContactOperationType.export);
await connector.sendFrame(exportContactFrame);
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
}
Future<void> _contactZeroHop(Uint8List pubKey) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
_pendingOperations.add(ContactOperationType.zeroHopShare);
await connector.sendFrame(exportContactZeroHopFrame);
await connector.sendFrame(
exportContactZeroHopFrame,
expectsGenericAck: true,
);
}
Future<void> _contactImport() async {
@@ -205,7 +220,7 @@ class _ContactsScreenState extends State<ContactsScreen>
try {
final importContactFrame = buildImportContactFrame(hexString);
_pendingOperations.add(ContactOperationType.import);
await connector.sendFrame(importContactFrame);
await connector.sendFrame(importContactFrame, expectsGenericAck: true);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -229,9 +244,7 @@ class _ContactsScreenState extends State<ContactsScreen>
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
leading: BatteryIndicator(connector: connector),
title: Text(context.l10n.contacts_title),
centerTitle: true,
title: AppBarTitle(context.l10n.contacts_title),
automaticallyImplyLeading: false,
actions: [
PopupMenuButton(
@@ -389,6 +402,41 @@ class _ContactsScreenState extends State<ContactsScreen>
? const <ContactGroup>[]
: _filterAndSortGroups(_groups, contacts);
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(
@@ -396,7 +444,7 @@ class _ContactsScreenState extends State<ContactsScreen>
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: context.l10n.contacts_searchContacts,
hintText: hintText,
prefixIcon: const Icon(Icons.search),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
@@ -468,6 +516,7 @@ class _ContactsScreenState extends State<ContactsScreen>
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
isFavorite: contact.isFavorite,
onTap: () => _openChat(context, contact),
onLongPress: () =>
_showContactOptions(context, connector, contact),
@@ -504,6 +553,8 @@ class _ContactsScreenState extends State<ContactsScreen>
})
.where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
// Groups don't have a favorite flag, so hide them under favorites filter
if (_typeFilter == ContactTypeFilter.favorites) return false;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
@@ -578,6 +629,8 @@ class _ContactsScreenState extends State<ContactsScreen>
switch (_typeFilter) {
case ContactTypeFilter.all:
return true;
case ContactTypeFilter.favorites:
return contact.isFavorite;
case ContactTypeFilter.users:
return contact.type == advTypeChat;
case ContactTypeFilter.repeaters:
@@ -968,6 +1021,7 @@ class _ContactsScreenState extends State<ContactsScreen>
) {
final isRepeater = contact.type == advTypeRepeater;
final isRoom = contact.type == advTypeRoom;
final isFavorite = contact.isFavorite;
showModalBottomSheet(
context: context,
@@ -1074,6 +1128,21 @@ class _ContactsScreenState extends State<ContactsScreen>
},
),
],
ListTile(
leading: Icon(
isFavorite ? Icons.star : Icons.star_border,
color: Colors.amber[700],
),
title: Text(
isFavorite
? context.l10n.listFilter_removeFromFavorites
: context.l10n.listFilter_addToFavorites,
),
onTap: () async {
Navigator.pop(sheetContext);
await connector.setContactFavorite(contact, !isFavorite);
},
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.contacts_ShareContact),
@@ -1142,6 +1211,7 @@ class _ContactTile extends StatelessWidget {
final Contact contact;
final DateTime lastSeen;
final int unreadCount;
final bool isFavorite;
final VoidCallback onTap;
final VoidCallback onLongPress;
@@ -1149,6 +1219,7 @@ class _ContactTile extends StatelessWidget {
required this.contact,
required this.lastSeen,
required this.unreadCount,
required this.isFavorite,
required this.onTap,
required this.onLongPress,
});
@@ -1160,12 +1231,17 @@ class _ContactTile extends StatelessWidget {
backgroundColor: _getTypeColor(contact.type),
child: _buildContactAvatar(contact),
),
title: Text(contact.name),
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(contact.pathLabel),
Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)),
Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis),
Text(
contact.shortPubKeyHex,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
],
),
// Clamp text scaling in trailing section to prevent overflow while
@@ -1176,6 +1252,8 @@ class _ContactTile extends StatelessWidget {
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
),
),
child: SizedBox(
width: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
@@ -1186,11 +1264,18 @@ class _ContactTile extends StatelessWidget {
],
Text(
_formatLastSeen(context, lastSeen),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isFavorite)
Icon(Icons.star, size: 14, color: Colors.amber[700]),
if (isFavorite && contact.hasLocation)
const SizedBox(width: 2),
if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
],
@@ -1198,6 +1283,7 @@ class _ContactTile extends StatelessWidget {
],
),
),
),
onTap: onTap,
onLongPress: onLongPress,
);
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -7,6 +7,7 @@ import '../l10n/app_localizations.dart';
import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
class MapCacheScreen extends StatefulWidget {
const MapCacheScreen({super.key});
@@ -224,7 +225,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
return Scaffold(
appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true),
appBar: AppBar(
title: AdaptiveAppBarTitle(l10n.mapCache_title),
centerTitle: true,
),
body: Column(
children: [
Expanded(
+360 -30
View File
@@ -1,13 +1,17 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../connector/meshcore_protocol.dart';
import '../models/app_settings.dart';
import '../models/channel.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
@@ -15,8 +19,8 @@ import '../services/map_marker_service.dart';
import '../services/map_tile_cache_service.dart';
import '../utils/contact_search.dart';
import '../utils/route_transitions.dart';
import '../widgets/battery_indicator.dart';
import '../widgets/quick_switch_bar.dart';
import '../icons/los_icon.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
import 'contacts_screen.dart';
@@ -24,6 +28,7 @@ import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
import 'line_of_sight_map_screen.dart';
class MapScreen extends StatefulWidget {
final LatLng? highlightPosition;
@@ -44,13 +49,21 @@ class MapScreen extends StatefulWidget {
}
class _MapScreenState extends State<MapScreen> {
static const double _labelZoomThreshold = 8.5;
final MapController _mapController = MapController();
final MapMarkerService _markerService = MapMarkerService();
final Set<String> _hiddenMarkerIds = {};
Set<String> _removedMarkerIds = {};
bool _isBuildingPathTrace = false;
bool _isSelectingPoi = false;
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
final List<int> _pathTrace = [];
final List<LatLng> _points = [];
final List<Polyline> _polylines = [];
bool _legendExpanded = false;
bool _showNodeLabels = true;
@override
void initState() {
@@ -98,7 +111,7 @@ class _MapScreenState extends State<MapScreen> {
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
final maxSpread = max(latStdDev, lonStdDev);
if (maxSpread <= 0) return 13.0;
// Approzimate: each zoom level halves the visible area
// Approximate: each zoom level halves the visible area
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
return zoom.clamp(4.0, 15.0);
@@ -147,6 +160,19 @@ class _MapScreenState extends State<MapScreen> {
.where((c) => c.hasLocation)
.toList();
_polylines.clear();
_polylines.addAll(
_points.length > 1
? [
Polyline(
points: _points,
strokeWidth: 4,
color: Colors.blueAccent,
),
]
: <Polyline>[],
);
// Calculate center and zoom of all nodes, or default to (0, 0)
LatLng center = const LatLng(0, 0);
double initialZoom = 10.0;
@@ -227,6 +253,7 @@ class _MapScreenState extends State<MapScreen> {
// Re center map after removed markers have loaded
if (!_hasInitializedMap && _removedMarkersLoaded) {
_hasInitializedMap = true;
_showNodeLabels = initialZoom >= _labelZoomThreshold;
if (hasMapContent) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@@ -242,11 +269,57 @@ class _MapScreenState extends State<MapScreen> {
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
leading: BatteryIndicator(connector: connector),
title: Text(context.l10n.map_title),
title: AppBarTitle(context.l10n.map_title),
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
IconButton(
icon: const LosIcon(),
onPressed: () {
final candidates = <LineOfSightEndpoint>[];
if (connector.selfLatitude != null &&
connector.selfLongitude != null) {
candidates.add(
LineOfSightEndpoint(
label: context.l10n.pathTrace_you,
point: LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
color: Colors.teal,
icon: Icons.person_pin_circle,
),
);
}
for (final c in contactsWithLocation) {
candidates.add(
LineOfSightEndpoint(
label: c.name,
point: LatLng(c.latitude!, c.longitude!),
color: _getNodeColor(c.type),
icon: _getNodeIcon(c.type),
),
);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LineOfSightMapScreen(
title: context.l10n.map_losScreenTitle,
candidates: candidates,
),
),
);
},
tooltip: context.l10n.map_lineOfSight,
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
@@ -325,6 +398,14 @@ class _MapScreenState extends State<MapScreen> {
position: latLng,
);
},
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (shouldShow != _showNodeLabels && mounted) {
setState(() {
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
@@ -334,6 +415,8 @@ class _MapScreenState extends State<MapScreen> {
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty && _isBuildingPathTrace)
PolylineLayer(polylines: _polylines),
MarkerLayer(
markers: [
if (highlightPosition != null)
@@ -347,7 +430,11 @@ class _MapScreenState extends State<MapScreen> {
size: 34,
),
),
..._buildMarkers(contactsWithLocation, settings),
..._buildMarkers(
contactsWithLocation,
settings,
showLabels: _showNodeLabels,
),
...sharedMarkers.map(_buildSharedMarker),
if (connector.selfLatitude != null &&
connector.selfLongitude != null)
@@ -356,8 +443,8 @@ class _MapScreenState extends State<MapScreen> {
connector.selfLatitude!,
connector.selfLongitude!,
),
width: 35,
height: 35,
width: 40,
height: 40,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
@@ -376,21 +463,34 @@ class _MapScreenState extends State<MapScreen> {
],
),
alignment: Alignment.center,
child: Text(
context.l10n.pathTrace_you,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 12,
child: const Icon(
Icons.person_pin_circle,
color: Colors.white,
size: 20,
),
),
),
if (_showNodeLabels &&
connector.selfLatitude != null &&
connector.selfLongitude != null)
_buildNodeLabelMarker(
point: LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
label: context.l10n.pathTrace_you,
),
],
),
],
),
_buildLegend(contactsWithLocation.length, sharedMarkers.length),
if (!_isBuildingPathTrace)
_buildLegend(
contactsWithLocation,
settings,
sharedMarkers.length,
),
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
],
),
bottomNavigationBar: SafeArea(
@@ -412,20 +512,28 @@ class _MapScreenState extends State<MapScreen> {
);
}
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
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) {
if (contact.type == advTypeRepeater &&
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
continue;
}
if (contact.type == advTypeChat &&
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
!settings.mapShowOtherNodes) {
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
continue;
}
@@ -434,7 +542,11 @@ class _MapScreenState extends State<MapScreen> {
width: 35,
height: 35,
child: GestureDetector(
onTap: () => _showNodeInfo(context, contact),
onLongPress: () =>
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
onTap: () => _isBuildingPathTrace
? _addToPath(context, contact)
: _showNodeInfo(context, contact),
child: Column(
children: [
Container(
@@ -463,11 +575,54 @@ class _MapScreenState extends State<MapScreen> {
);
markers.add(marker);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: LatLng(contact.latitude!, contact.longitude!),
label: contact.name,
),
);
}
}
return markers;
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
Color _getNodeColor(int type) {
switch (type) {
case advTypeChat:
@@ -498,16 +653,47 @@ class _MapScreenState extends State<MapScreen> {
}
}
Widget _buildLegend(int nodeCount, int markerCount) {
Widget _buildLegend(
List<Contact> contactsWithLocation,
settings,
int markerCount,
) {
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++;
}
return Positioned(
top: 16,
right: 16,
child: Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
setState(() {
_legendExpanded = !_legendExpanded;
});
},
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.map_nodesCount(nodeCount),
@@ -519,11 +705,30 @@ class _MapScreenState extends State<MapScreen> {
Text(
context.l10n.map_pinsCount(markerCount),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
const SizedBox(height: 8),
],
),
const SizedBox(width: 8),
AnimatedRotation(
turns: _legendExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
child: const Icon(Icons.expand_more, size: 20),
),
],
),
),
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 6),
_buildLegendItem(
Icons.person,
context.l10n.map_chat,
@@ -544,7 +749,11 @@ class _MapScreenState extends State<MapScreen> {
context.l10n.map_sensor,
Colors.orange,
),
_buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinDm,
Colors.blue,
),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPrivate,
@@ -558,13 +767,20 @@ class _MapScreenState extends State<MapScreen> {
],
),
),
crossFadeState: _legendExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
],
),
),
);
}
Widget _buildLegendItem(IconData icon, String label, Color color) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
padding: const EdgeInsets.symmetric(vertical: 1.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -747,7 +963,7 @@ class _MapScreenState extends State<MapScreen> {
color: _getNodeColor(contact.type),
),
const SizedBox(width: 8),
Expanded(child: Text(contact.name)),
Expanded(child: SelectableText(contact.name)),
],
),
content: Column(
@@ -918,7 +1134,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
const SizedBox(height: 2),
Text(value, style: const TextStyle(fontSize: 14)),
SelectableText(value, style: const TextStyle(fontSize: 14)),
],
),
);
@@ -1085,7 +1301,8 @@ class _MapScreenState extends State<MapScreen> {
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: TextField(
decoration: InputDecoration(
hintText: context.l10n.contacts_searchContacts,
hintText:
context.l10n.contacts_searchContactsNoNumber,
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -1412,6 +1629,119 @@ class _MapScreenState extends State<MapScreen> {
return context.l10n.time_allTime;
}
}
void _addToPath(BuildContext context, Contact contact) {
setState(() {
_pathTrace.add(
contact.publicKey[0],
); // Add first 16 bytes of public key to path trace
_points.add(LatLng(contact.latitude!, contact.longitude!));
});
}
void _startPath() {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_points.clear();
_polylines.clear();
});
}
void _removePath() {
setState(() {
_pathTrace.removeLast(); // Remove last node from path trace
_points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines
});
}
Widget _buildPathTraceOverlay() {
final l10n = context.l10n;
final isImperial =
context.read<AppSettingsService>().settings.unitSystem ==
UnitSystem.imperial;
return Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.contacts_pathTrace,
style: TextStyle(fontWeight: FontWeight.bold),
),
if (_pathTrace.isEmpty) const SizedBox(height: 8),
if (_pathTrace.isEmpty)
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
const SizedBox(height: 6),
if (_pathTrace.isNotEmpty)
Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
),
SelectableText(
_pathTrace
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(','),
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
if (_pathTrace.isNotEmpty)
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
),
),
);
setState(() {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
),
if (_pathTrace.isNotEmpty)
ElevatedButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
),
if (_pathTrace.isEmpty)
ElevatedButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
_pathTrace.clear();
_points.clear();
_polylines.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
child: Text(l10n.common_cancel),
),
],
),
],
),
),
),
);
}
}
class _MarkerPayload {
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
@@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart';
class NeighboursScreen extends StatefulWidget {
class NeighborsScreen extends StatefulWidget {
final Contact repeater;
final String password;
const NeighboursScreen({
const NeighborsScreen({
super.key,
required this.repeater,
required this.password,
});
@override
State<NeighboursScreen> createState() => _NeighboursScreenState();
State<NeighborsScreen> createState() => _NeighborsScreenState();
}
class _NeighboursScreenState extends State<NeighboursScreen> {
static const int _reqNeighboursKeyLen = 4;
class _NeighborsScreenState extends State<NeighborsScreen> {
static const int _reqNeighborsKeyLen = 4;
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _neighbourCount = 0;
int _neighborCount = 0;
bool _isLoading = false;
bool _isLoaded = false;
@@ -41,7 +42,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbours;
List<Map<String, dynamic>>? _parsedNeighbors;
@override
void initState() {
@@ -49,7 +50,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
_loadNeighbours();
_loadNeighbors();
_hasData = false;
}
@@ -62,13 +63,12 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleNeighboursResponse(connector, frame.sublist(6));
_handleNeighborsResponse(connector, frame.sublist(6));
}
});
}
@@ -91,13 +91,14 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
return '${h}h ${m2}m';
}
static List<Map<String, dynamic>> parseNeighboursData(
static List<Map<String, dynamic>> parseNeighborsData(
BufferReader buffer,
int resultsCount,
) {
final Map<int, Map<String, dynamic>> neighbours = {};
final Map<int, Map<String, dynamic>> neighbors = {};
try {
for (var i = 0; i < resultsCount; i++) {
final neighbourData = neighbours.putIfAbsent(
final neighborData = neighbors.putIfAbsent(
i,
() => {
'contact': null,
@@ -106,35 +107,43 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
'snr': <double>{},
},
);
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
neighbourData['lastHeard'] = buffer.readUInt32LE();
neighbourData['snr'] = buffer.readInt8() / 4.0;
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
neighborData['lastHeard'] = buffer.readUInt32LE();
neighborData['snr'] = buffer.readInt8() / 4.0;
}
return neighbours.values.toList();
return neighbors.values.toList();
} catch (e) {
appLogger.error(
'Error parsing neighbors data: $e',
tag: 'NeighborsScreen',
);
return [];
}
}
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final neighbourCount = buffer.readUInt16LE();
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
try {
final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
for (var neighbourData in parsedNeighbours) {
final publicKey = neighbourData['publicKey'];
for (var neighborData in parsedNeighbors) {
final publicKey = neighborData['publicKey'];
if (listEquals(
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
repeater.publicKey.sublist(0, _reqNeighborsKeyLen),
publicKey,
)) {
neighbourData['contact'] = repeater;
neighborData['contact'] = repeater;
}
}
});
setState(() {
_parsedNeighbours = parsedNeighbours;
_neighbourCount = neighbourCount;
_parsedNeighbors = parsedNeighbors;
_neighborCount = neighborCount;
});
ScaffoldMessenger.of(context).showSnackBar(
@@ -150,6 +159,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
_isLoaded = true;
_hasData = true;
});
} catch (e) {
appLogger.error('Error handling neighbors response: $e');
}
}
Contact _resolveRepeater(MeshCoreConnector connector) {
@@ -159,7 +171,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
);
}
Future<void> _loadNeighbours() async {
Future<void> _loadNeighbors() async {
if (_commandService == null) return;
setState(() {
@@ -172,17 +184,17 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
final selection = await connector.preparePathForContactSend(repeater);
_pendingStatusSelection = selection;
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
//[version][number of requested neighbors][offset_16bit][order by][len of public key]
final frame = buildSendBinaryReq(
repeater.publicKey,
payload: Uint8List.fromList([
reqTypeGetNeighbours,
reqTypeGetNeighbors,
0x00,
0x0F,
0x00,
0x00,
0x00,
_reqNeighboursKeyLen,
_reqNeighborsKeyLen,
]),
);
await connector.sendFrame(frame);
@@ -258,7 +270,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.neighbors_repeatersNeighbours,
l10n.neighbors_repeatersNeighbors,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
@@ -345,7 +357,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadNeighbours,
onPressed: _isLoading ? null : _loadNeighbors,
tooltip: l10n.repeater_refresh,
),
],
@@ -353,13 +365,13 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadNeighbours,
onRefresh: _loadNeighbors,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
if (!_isLoaded &&
!_hasData &&
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
Center(
child: Text(
l10n.neighbors_noData,
@@ -368,10 +380,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
),
if (_isLoaded ||
_hasData &&
!(_parsedNeighbours == null ||
_parsedNeighbours!.isEmpty))
_buildNeighboursInfoCard(
"${l10n.repeater_neighbours} - $_neighbourCount",
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
_buildNeighborsInfoCard(
"${l10n.repeater_neighbors} - $_neighborCount",
),
],
),
@@ -380,7 +391,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
);
}
Widget _buildNeighboursInfoCard(String title) {
Widget _buildNeighborsInfoCard(String title) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
return Card(
child: Padding(
@@ -405,7 +416,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
],
),
const Divider(),
for (final entry in _parsedNeighbours!.asMap().entries)
for (final entry in _parsedNeighbors!.asMap().entries)
_buildInfoRow(
entry.value['contact'] != null
? entry.value['contact'].name
@@ -430,6 +441,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
double snr,
int spreadingFactor,
) {
final snrUi = snrUiFromSNR(snr, spreadingFactor);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
@@ -443,9 +455,15 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(value),
trailing: SNRIcon(
snr: snr,
snrLevels: getSNRfromSF(spreadingFactor),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
Text(
snrUi.text,
style: TextStyle(fontSize: 10, color: snrUi.color),
),
],
),
),
),
+248 -107
View File
@@ -8,14 +8,37 @@ import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/services/app_settings_service.dart';
import 'package:meshcore_open/services/map_tile_cache_service.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/snr_indicator.dart';
import 'package:provider/provider.dart';
double getPathDistanceMeters(List<LatLng> points) {
if (points.length <= 1) return 0.0;
double distanceMeters = 0.0;
final distanceCalculator = Distance();
for (int i = 0; i < points.length - 1; i++) {
distanceMeters += distanceCalculator(points[i], points[i + 1]);
}
return distanceMeters;
}
String formatDistance(double distanceMeters, {required bool isImperial}) {
if (isImperial) {
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
}
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
}
class PathTraceData {
final Uint8List pathData;
final Uint8List snrData;
final List<double> snrData;
final Map<int, Contact> pathContacts;
PathTraceData({
@@ -28,6 +51,7 @@ class PathTraceData {
class PathTraceMapScreen extends StatefulWidget {
final String title;
final Uint8List path;
final int? repeaterId;
final bool flipPathRound;
final bool reversePathRound;
@@ -35,6 +59,7 @@ class PathTraceMapScreen extends StatefulWidget {
super.key,
required this.title,
required this.path,
this.repeaterId,
this.flipPathRound = false,
this.reversePathRound = false,
});
@@ -44,13 +69,14 @@ class PathTraceMapScreen extends StatefulWidget {
}
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
static const double _labelZoomThreshold = 8.5;
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
bool _isLoading = false;
bool _failed2Loaded = false;
bool _hasData = false;
bool _noLocationErr = false;
PathTraceData? _traceData;
List<LatLng> _points = <LatLng>[];
List<Polyline> _polylines = [];
@@ -58,7 +84,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
double _initialZoom = 2.0;
LatLngBounds? _bounds;
ValueKey<String> _mapKey = const ValueKey('initial');
double _pathDistance = 0.0;
double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true;
String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes
@@ -80,7 +107,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
super.dispose();
}
Uint8List addReturnpath(Uint8List pathBytes) {
Uint8List addReturnPath(Uint8List pathBytes) {
Uint8List? traceBytes;
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
@@ -93,23 +120,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
return traceBytes;
}
double getPathDistance() {
double totalDistance = 0.0;
final distanceCalculator = Distance();
for (int i = 0; i < _points.length - 1; i++) {
totalDistance += distanceCalculator(_points[i], _points[i + 1]);
}
return totalDistance;
}
Future<void> _doPathTrace() async {
if (mounted) {
setState(() {
_isLoading = true;
_failed2Loaded = false;
_noLocationErr = false;
});
}
@@ -120,7 +135,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
: widget.path;
if (widget.flipPathRound) {
path = addReturnpath(pathTmp);
path = addReturnPath(pathTmp);
} else {
path = pathTmp;
}
@@ -142,21 +157,34 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final frameBuffer = BufferReader(frame);
try {
final code = frameBuffer.readUInt8();
if (code == respCodeSent) {
frameBuffer.skipBytes(1); //reserved
tagData = frameBuffer.readBytes(4);
final timeoutSeconds = frameBuffer.readUInt32LE();
final timeoutMilliseconds = frameBuffer.readUInt32LE();
// Start timeout timer for trace response
_timeoutTimer?.cancel();
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
_timeoutTimer = Timer(
Duration(milliseconds: timeoutMilliseconds),
() {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
},
);
}
if (code == respCodeErr) {
_timeoutTimer?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
}
@@ -171,6 +199,16 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
_handleTraceResponse(frame);
}
}
} catch (e) {
_timeoutTimer?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
// Handle any parsing errors gracefully
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
}
});
}
@@ -178,16 +216,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final buffer = BufferReader(frame);
try {
buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8();
buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code
Uint8List pathData = buffer.readBytes(pathLength);
Uint8List snrData = buffer.readRemainingBytes();
List<double> snrData = buffer
.readRemainingBytes()
.map((snr) => snr.toSigned(8).toDouble() / 4)
.toList();
Map<int, Contact> pathContacts = {};
connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
connector.contacts.where((c) => c.type != advTypeChat).forEach((
repeater,
) {
for (var repeaterData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
@@ -215,8 +259,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
contact.latitude != null &&
contact.longitude != null) {
_points.add(LatLng(contact.latitude!, contact.longitude!));
} else {
_noLocationErr = true;
}
}
_polylines = _points.length > 1
@@ -229,20 +271,36 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
]
: <Polyline>[];
_initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0);
_initialCenter = _points.isNotEmpty
? _points.first
: const LatLng(0, 0);
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
_mapKey = ValueKey(
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
);
_pathDistance = getPathDistance();
_pathDistanceMeters = getPathDistanceMeters(_points);
});
} catch (e) {
appLogger.error(
'Error handling trace response: $e',
tag: 'PathTraceMapScreen',
);
if (mounted) {
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
}
}
}
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
return Scaffold(
@@ -279,20 +337,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
top: false,
child: Stack(
children: [
if (_noLocationErr)
Center(
child: Card(
color: Colors.red,
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
context.l10n.pathTrace_someHopsNoLocation,
style: TextStyle(color: Colors.white),
),
),
),
),
if (!_hasData && !_noLocationErr)
if (!_hasData)
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -304,43 +349,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
],
),
),
if (_hasData && !_noLocationErr)
FlutterMap(
key: _mapKey,
options: MapOptions(
initialCenter: _initialCenter!,
initialZoom: _initialZoom,
initialCameraFit: _bounds == null
? null
: CameraFit.bounds(
bounds: _bounds!,
padding: const EdgeInsets.all(64),
maxZoom: 16,
),
minZoom: 2.0,
maxZoom: 18.0,
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty)
PolylineLayer(polylines: _polylines),
if (_traceData!.pathData.isNotEmpty)
MarkerLayer(
markers: _buildHopMarkers(_traceData!.pathData),
),
],
),
if (_hasData) _buildMapPathTrace(context, tileCache),
if (_points.isEmpty &&
!_hasData &&
!_isLoading &&
!_failed2Loaded &&
!_noLocationErr)
!_failed2Loaded)
Center(
child: Card(
color: Colors.white.withValues(alpha: 0.9),
@@ -352,8 +365,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
if (_hasData && !_noLocationErr)
_buildLegendCard(context, _traceData!),
if (_hasData)
_buildLegendCard(context, _traceData!, isImperial),
],
),
),
@@ -362,15 +375,18 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
);
}
List<Marker> _buildHopMarkers(List<int> pathData) {
return [
for (final hop in pathData)
if (_traceData!.pathContacts[hop]!.hasLocation)
List<Marker> _buildHopMarkers(
List<int> pathData, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop];
if (contact == null || !contact.hasLocation) continue;
final point = LatLng(contact.latitude!, contact.longitude!);
markers.add(
Marker(
point: LatLng(
_traceData!.pathContacts[hop]!.latitude!,
_traceData!.pathContacts[hop]!.longitude!,
),
point: point,
width: 35,
height: 35,
child: Container(
@@ -389,11 +405,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
alignment: Alignment.center,
child: Text(
_traceData!.pathContacts[hop]!.publicKey
contact.publicKey
.sublist(0, 1)
.map(
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
)
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(),
style: const TextStyle(
color: Colors.white,
@@ -403,13 +417,19 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
if (context.read<MeshCoreConnector>().selfLatitude != null &&
context.read<MeshCoreConnector>().selfLongitude != null)
);
if (showLabels) {
markers.add(_buildNodeLabelMarker(point: point, label: contact.name));
}
}
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
if (selfLat != null && selfLon != null) {
final selfPoint = LatLng(selfLat, selfLon);
markers.add(
Marker(
point: LatLng(
context.read<MeshCoreConnector>().selfLatitude!,
context.read<MeshCoreConnector>().selfLongitude!,
),
point: selfPoint,
width: 35,
height: 35,
child: Container(
@@ -437,7 +457,53 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
];
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
return markers;
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
String formatDirectionText(PathTraceData pathTraceData, int index) {
@@ -453,7 +519,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null ? "$hex: $contactName" : hex;
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
}
} else {
final contactName =
@@ -462,7 +530,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null ? "$hex: $contactName" : hex;
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
}
}
@@ -475,7 +545,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null ? "$hex: $contactName" : hex;
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
} else {
return context.l10n.pathTrace_you;
}
@@ -486,11 +558,64 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null ? "$hex: $contactName" : hex;
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
}
}
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
Widget _buildMapPathTrace(
BuildContext context,
MapTileCacheService tileCache,
) {
return FlutterMap(
key: _mapKey,
options: MapOptions(
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
initialCenter: _initialCenter!,
initialZoom: _initialZoom,
initialCameraFit: _bounds == null
? null
: CameraFit.bounds(
bounds: _bounds!,
padding: const EdgeInsets.all(64),
maxZoom: 16,
),
minZoom: 2.0,
maxZoom: 18.0,
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (shouldShow != _showNodeLabels && mounted) {
setState(() {
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName: MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
if (_traceData!.pathData.isNotEmpty)
MarkerLayer(
markers: _buildHopMarkers(
_traceData!.pathData,
showLabels: _showNodeLabels,
),
),
],
);
}
Widget _buildLegendCard(
BuildContext context,
PathTraceData pathTraceData,
bool isImperial,
) {
final l10n = context.l10n;
final maxHeight = MediaQuery.of(context).size.height * 0.35;
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
@@ -509,7 +634,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
@@ -523,8 +648,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: pathTraceData.pathData.length + 1,
separatorBuilder: (_, __) => const Divider(height: 1),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final snrUi = snrUiFromSNR(
index < pathTraceData.snrData.length
? pathTraceData.snrData[index]
: null,
context.read<MeshCoreConnector>().currentSf,
);
return Column(
children: [
ListTile(
@@ -543,12 +674,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
style: const TextStyle(fontSize: 14),
),
trailing: SNRIcon(
snr:
pathTraceData.snrData[index].toSigned(
8,
) /
4.0,
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
snrUi.icon,
color: snrUi.color,
size: 18.0,
),
Text(
snrUi.text,
style: TextStyle(
fontSize: 10,
color: snrUi.color,
),
),
],
),
onTap: () {
// Handle item tap
+1
View File
@@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
_commandController.clear();
_historyIndex = -1;
_commandFocusNode.requestFocus();
// Auto-scroll to bottom
Future.delayed(const Duration(milliseconds: 100), () {
+67 -7
View File
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
import 'repeater_status_screen.dart';
import 'repeater_cli_screen.dart';
import 'repeater_settings_screen.dart';
import 'telemetry_screen.dart';
import 'neighbours_screen.dart';
import 'neighbors_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
@@ -21,6 +23,10 @@ class RepeaterHubScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final settingsService = context.watch<AppSettingsService>();
final chemistry = settingsService.batteryChemistryForRepeater(
repeater.publicKeyHex,
);
return Scaffold(
appBar: AppBar(
title: Column(
@@ -107,6 +113,62 @@ class RepeaterHubScreen extends StatelessWidget {
),
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.battery_full),
const SizedBox(width: 10),
Expanded(
child: Text(
l10n.appSettings_batteryChemistry,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: chemistry,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
),
onChanged: (value) {
if (value == null) return;
settingsService.setBatteryChemistryForRepeater(
repeater.publicKeyHex,
value,
);
},
items: [
DropdownMenuItem(
value: 'nmc',
child: Text(l10n.appSettings_batteryNmc),
),
DropdownMenuItem(
value: 'lifepo4',
child: Text(l10n.appSettings_batteryLifepo4),
),
DropdownMenuItem(
value: 'lipo',
child: Text(l10n.appSettings_batteryLipo),
),
],
),
],
),
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
@@ -174,17 +236,15 @@ class RepeaterHubScreen extends StatelessWidget {
_buildManagementCard(
context,
icon: Icons.group,
title: l10n.repeater_neighbours,
subtitle: l10n.repeater_neighboursSubtitle,
title: l10n.repeater_neighbors,
subtitle: l10n.repeater_neighborsSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NeighboursScreen(
repeater: repeater,
password: password,
),
builder: (context) =>
NeighborsScreen(repeater: repeater, password: password),
),
);
},
+35 -9
View File
@@ -8,7 +8,9 @@ import '../models/contact.dart';
import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../utils/battery_utils.dart';
import '../widgets/path_management_dialog.dart';
class RepeaterStatusScreen extends StatefulWidget {
@@ -179,6 +181,12 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_dupDirect = directDups;
_dupFlood = floodDups;
});
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.updateRepeaterBatterySnapshot(
widget.repeater.publicKeyHex,
batteryMv,
source: 'status_binary',
);
_recordStatusResult(true);
}
@@ -201,6 +209,18 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_uptimeSecs = _asInt(data['uptime_secs']);
_queueLen = _asInt(data['queue_len']);
_debugFlags = _asInt(data['errors']);
final batteryMv = _batteryMv;
if (batteryMv != null) {
final connector = Provider.of<MeshCoreConnector>(
context,
listen: false,
);
connector.updateRepeaterBatterySnapshot(
widget.repeater.publicKeyHex,
batteryMv,
source: 'status_text',
);
}
} else if (data.containsKey('noise_floor')) {
_noiseFloor = _asInt(data['noise_floor']);
_lastRssi = _asInt(data['last_rssi']);
@@ -590,18 +610,24 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
}
String _batteryText() {
if (_batteryMv == null) return '';
final percent = _batteryPercentFromMv(_batteryMv!);
final volts = (_batteryMv! / 1000.0).toStringAsFixed(2);
final connector = context.watch<MeshCoreConnector>();
final batteryMv =
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
_batteryMv;
if (batteryMv == null) return '';
final percent = estimateBatteryPercentFromMillivolts(
batteryMv,
_batteryChemistry(),
);
final volts = (batteryMv / 1000.0).toStringAsFixed(2);
return '$percent% / ${volts}V';
}
int _batteryPercentFromMv(int millivolts) {
const minMv = 3000;
const maxMv = 4200;
if (millivolts <= minMv) return 0;
if (millivolts >= maxMv) return 100;
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
String _batteryChemistry() {
final settingsService = context.read<AppSettingsService>();
return settingsService.batteryChemistryForRepeater(
widget.repeater.publicKeyHex,
);
}
String _clockText() {
+71 -2
View File
@@ -1,9 +1,13 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/material.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 '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
import 'contacts_screen.dart';
@@ -18,6 +22,8 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false;
late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
@override
void initState() {
@@ -39,12 +45,25 @@ class _ScannerScreenState extends State<ScannerScreen> {
};
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());
}
}
});
}
@override
void dispose() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.removeListener(_connectionListener);
unawaited(_bluetoothStateSubscription.cancel());
super.dispose();
}
@@ -52,7 +71,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.scanner_title),
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
centerTitle: true,
automaticallyImplyLeading: false,
),
@@ -62,6 +81,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
builder: (context, connector, child) {
return Column(
children: [
// Bluetooth off warning
if (_bluetoothState == BluetoothAdapterState.off)
_bluetoothOffWarning(context),
// Status bar
_buildStatusBar(context, connector),
@@ -76,9 +99,12 @@ class _ScannerScreenState extends State<ScannerScreen> {
builder: (context, connector, child) {
final isScanning =
connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
return FloatingActionButton.extended(
onPressed: () {
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
@@ -205,4 +231,47 @@ class _ScannerScreenState extends State<ScannerScreen> {
}
}
}
Widget _bluetoothOffWarning(BuildContext context) {
final errorColor = Theme.of(context).colorScheme.error;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
color: errorColor.withValues(alpha: 0.15),
child: Row(
children: [
Icon(Icons.bluetooth_disabled, size: 24, color: errorColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.scanner_bluetoothOff,
style: TextStyle(
color: errorColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
context.l10n.scanner_bluetoothOffMessage,
style: TextStyle(
color: errorColor.withValues(alpha: 0.85),
fontSize: 12,
),
),
],
),
),
if (Platform.isAndroid)
TextButton(
onPressed: () => FlutterBluePlus.turnOn(),
child: Text(context.l10n.scanner_enableBluetooth),
),
],
),
);
}
}
+125 -58
View File
@@ -8,6 +8,7 @@ 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 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
@@ -21,6 +22,7 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
bool _showBatteryVoltage = false;
bool _deviceInfoExpanded = false;
String _appVersion = '';
@override
@@ -40,7 +42,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
appBar: AppBar(
title: AdaptiveAppBarTitle(l10n.settings_title),
centerTitle: true,
),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
@@ -74,18 +79,52 @@ class _SettingsScreenState extends State<SettingsScreen> {
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
setState(() {
_deviceInfoExpanded = !_deviceInfoExpanded;
});
},
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Row(
children: [
Expanded(
child: Text(
l10n.settings_deviceInfo,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
AnimatedRotation(
turns: _deviceInfoExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
child: const Icon(Icons.expand_more),
),
],
),
),
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
l10n.settings_infoName,
connector.deviceDisplayName,
),
const SizedBox(height: 16),
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
_buildInfoRow(
l10n.settings_infoStatus,
@@ -112,6 +151,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
],
),
),
crossFadeState: _deviceInfoExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
],
),
);
}
@@ -355,22 +401,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
Color? valueColor,
VoidCallback? onTap,
}) {
final theme = Theme.of(context);
final row = Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (leading != null) ...[leading, const SizedBox(width: 8)],
Text(label, style: TextStyle(color: Colors.grey[600])),
Expanded(
child: Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
],
),
Flexible(
child: Text(
const SizedBox(height: 4),
Text(
value,
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyLarge?.copyWith(
color: valueColor,
fontWeight: FontWeight.w500,
),
),
],
@@ -379,11 +436,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (onTap != null) {
return InkWell(
borderRadius: BorderRadius.circular(6),
onTap: onTap,
borderRadius: BorderRadius.circular(4),
child: row,
);
}
return row;
}
@@ -688,7 +746,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
_gpxExport(
Future<void> _gpxExport(
GpxExport exporter,
String name,
String description,
@@ -728,7 +786,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
_buildExportCard(MeshCoreConnector connector) {
Widget _buildExportCard(MeshCoreConnector connector) {
final l10n = context.l10n;
return Card(
child: Column(
@@ -808,6 +866,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7;
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
final _txPowerController = TextEditingController(text: '20');
bool _clientRepeat = false;
@override
void initState() {
@@ -857,6 +916,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (widget.connector.currentTxPower != null) {
_txPowerController.text = widget.connector.currentTxPower.toString();
}
_clientRepeat = widget.connector.clientRepeat ?? false;
}
@override
@@ -906,9 +967,29 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
widget.connector.currentCr,
);
// if the client repeat isnt null then we know its supported
//otherwise we leave it out of the frame to avoid accidentally enabling
final knownRepeat = widget.connector.clientRepeat != null;
if (knownRepeat) {
const validRepeatFreqsKHz = {433000, 869000, 918000};
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
);
return;
}
}
try {
await widget.connector.sendFrame(
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
buildSetRadioParamsFrame(
freqHz,
bwHz,
sf,
cr,
clientRepeat: knownRepeat ? _clientRepeat : null,
),
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
@@ -947,37 +1028,25 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.settings_presets,
style: const TextStyle(fontWeight: FontWeight.bold),
DropdownButtonFormField<int>(
decoration: InputDecoration(
labelText: l10n.settings_presets,
border: const OutlineInputBorder(),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
_PresetChip(
label: l10n.settings_preset915Mhz,
onTap: () => _applyPreset(RadioSettings.preset915MHz),
),
_PresetChip(
label: l10n.settings_preset868Mhz,
onTap: () => _applyPreset(RadioSettings.preset868MHz),
),
_PresetChip(
label: l10n.settings_preset433Mhz,
onTap: () => _applyPreset(RadioSettings.preset433MHz),
),
_PresetChip(
label: l10n.settings_longRange,
onTap: () => _applyPreset(RadioSettings.presetLongRange),
),
_PresetChip(
label: l10n.settings_fastSpeed,
onTap: () => _applyPreset(RadioSettings.presetFastSpeed),
items: [
for (var i = 0; i < RadioSettings.presets.length; i++)
DropdownMenuItem(
value: i,
child: Text(RadioSettings.presets[i].$1),
),
],
onChanged: (index) {
if (index != null) {
_applyPreset(RadioSettings.presets[index].$2);
}
},
),
const SizedBox(height: 24),
const SizedBox(height: 16),
TextField(
controller: _frequencyController,
decoration: InputDecoration(
@@ -1049,6 +1118,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
),
keyboardType: TextInputType.number,
),
if (widget.connector.clientRepeat != null) ...[
const SizedBox(height: 16),
SwitchListTile(
title: Text(l10n.settings_clientRepeat),
subtitle: Text(l10n.settings_clientRepeatSubtitle),
value: _clientRepeat,
onChanged: (value) => setState(() => _clientRepeat = value),
contentPadding: EdgeInsets.zero,
),
],
],
),
),
@@ -1062,15 +1141,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
);
}
}
class _PresetChip extends StatelessWidget {
final String label;
final VoidCallback onTap;
const _PresetChip({required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return ActionChip(label: Text(label), onPressed: onTap);
}
}
+49 -17
View File
@@ -5,11 +5,14 @@ import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../models/app_settings.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart';
class TelemetryScreen extends StatefulWidget {
final Contact repeater;
@@ -72,9 +75,19 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
}
void _handleStatusResponse(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,
batteryMv,
source: 'telemetry',
);
}
if (!mounted) return;
setState(() {
_parsedTelemetry = CayenneLpp.parseByChannel(frame);
_parsedTelemetry = parsedTelemetry;
});
ScaffoldMessenger.of(context).showSnackBar(
@@ -181,6 +194,8 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
Widget build(BuildContext context) {
final l10n = context.l10n;
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;
@@ -307,6 +322,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
entry['values'],
l10n.telemetry_channelTitle(entry['channel']),
entry['channel'],
isImperialUnits,
),
],
),
@@ -319,6 +335,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
Map<String, dynamic> channelData,
String title,
int channel,
bool isImperialUnits,
) {
final l10n = context.l10n;
return Card(
@@ -358,12 +375,12 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
else if (entry.key == 'temperature' && channel == 1)
_buildInfoRow(
l10n.telemetry_mcuTemperatureLabel,
_temperatureText(entry.value),
_temperatureText(entry.value, isImperialUnits),
)
else if (entry.key == 'temperature')
_buildInfoRow(
l10n.telemetry_temperatureLabel,
_temperatureText(entry.value),
_temperatureText(entry.value, isImperialUnits),
)
else if (entry.key == 'current' && channel == 1)
_buildInfoRow(
@@ -405,29 +422,44 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
);
}
String _batteryText(double? batteryMv) {
int? _extractTelemetryBatteryMillivolts(List<Map<String, dynamic>> entries) {
for (final entry in entries) {
if (entry['channel'] != 1) continue;
final values = entry['values'];
if (values is! Map<String, dynamic>) continue;
final voltage = values['voltage'];
if (voltage is num) return (voltage.toDouble() * 1000).round();
}
return null;
}
String _batteryText(double? telemetryVolts) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final batteryMv =
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
if (batteryMv == null) return l10n.common_notAvailable;
final percent = _batteryPercentFromMv(batteryMv);
final volts = batteryMv.toStringAsFixed(2);
final chemistry = _batteryChemistry();
final percent = estimateBatteryPercentFromMillivolts(batteryMv, chemistry);
final volts = (batteryMv / 1000).toStringAsFixed(2);
return l10n.telemetry_batteryValue(percent, volts);
}
int _batteryPercentFromMv(double millivolts) {
const minMv = 2.800;
const maxMv = 4.200;
if (millivolts <= minMv) return 0;
if (millivolts >= maxMv) return 100;
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
String _batteryChemistry() {
final settingsService = context.read<AppSettingsService>();
return settingsService.batteryChemistryForRepeater(
widget.repeater.publicKeyHex,
);
}
String _temperatureText(double? tempC) {
String _temperatureText(double? tempC, bool isImperialUnits) {
final l10n = context.l10n;
if (tempC == null) return l10n.common_notAvailable;
final tempF = (tempC * 9 / 5) + 32;
return l10n.telemetry_temperatureValue(
tempC.toStringAsFixed(1),
tempF.toStringAsFixed(1),
);
if (isImperialUnits) {
return '${tempF.toStringAsFixed(1)}°F';
}
return '${tempC.toStringAsFixed(1)}°C';
}
}
+42
View File
@@ -17,6 +17,12 @@ class AppSettingsService extends ChangeNotifier {
return stored ?? 'nmc';
}
String batteryChemistryForRepeater(String repeaterPubKeyHex) {
final stored = _settings.batteryChemistryByRepeaterId[repeaterPubKeyHex];
if (stored == 'liion') return 'nmc';
return stored ?? 'nmc';
}
Future<void> loadSettings() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_settingsKey);
@@ -74,6 +80,10 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(mapShowMarkers: value));
}
Future<void> setEnableMessageTracing(bool value) async {
await updateSettings(_settings.copyWith(enableMessageTracing: value));
}
Future<void> setMapCacheBounds(Map<String, double>? value) async {
await updateSettings(_settings.copyWith(mapCacheBounds: value));
}
@@ -132,4 +142,36 @@ class AppSettingsService extends ChangeNotifier {
_settings.copyWith(batteryChemistryByDeviceId: updated),
);
}
Future<void> setBatteryChemistryForRepeater(
String repeaterPubKeyHex,
String chemistry,
) async {
final updated = Map<String, String>.from(
_settings.batteryChemistryByRepeaterId,
);
updated[repeaterPubKeyHex] = chemistry;
await updateSettings(
_settings.copyWith(batteryChemistryByRepeaterId: updated),
);
}
Future<void> setUnitSystem(UnitSystem value) async {
await updateSettings(_settings.copyWith(unitSystem: value));
}
bool isChannelMuted(String channelName) {
return _settings.mutedChannels.contains(channelName);
}
Future<void> muteChannel(String channelName) async {
final updated = Set<String>.from(_settings.mutedChannels)..add(channelName);
await updateSettings(_settings.copyWith(mutedChannels: updated));
}
Future<void> unmuteChannel(String channelName) async {
final updated = Set<String>.from(_settings.mutedChannels)
..remove(channelName);
await updateSettings(_settings.copyWith(mutedChannels: updated));
}
}
+5 -12
View File
@@ -1,4 +1,3 @@
import 'dart:isolate';
import 'dart:io';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
@@ -15,20 +14,14 @@ class BackgroundService {
channelDescription: 'Keeps MeshCore running in the background.',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
iconData: const NotificationIconData(
resType: ResourceType.mipmap,
resPrefix: ResourcePrefix.ic,
name: 'launcher',
),
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: false,
playSound: false,
),
foregroundTaskOptions: const ForegroundTaskOptions(
interval: 5000,
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(5000),
autoRunOnBoot: false,
allowWakeLock: true,
allowWifiLock: false,
),
);
@@ -64,13 +57,13 @@ void startCallback() {
class _MeshCoreTaskHandler extends TaskHandler {
@override
void onStart(DateTime timestamp, SendPort? sendPort) {}
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
@override
void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {}
void onRepeatEvent(DateTime timestamp) {}
@override
void onDestroy(DateTime timestamp, SendPort? sendPort) {}
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
@override
void onNotificationButtonPressed(String id) {}
+21 -1
View File
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import '../connector/meshcore_protocol.dart';
class BleDebugLogEntry {
@@ -44,6 +45,7 @@ class BleDebugLogService extends ChangeNotifier {
static const int maxEntries = 500;
final List<BleDebugLogEntry> _entries = [];
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
bool _notifyScheduled = false;
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
List<BleRawLogRxEntry> get rawLogRxEntries =>
@@ -78,13 +80,31 @@ class BleDebugLogService extends ChangeNotifier {
}
}
notifyListeners();
_notifyListenersSafely();
}
void clear() {
_entries.clear();
_rawLogRxEntries.clear();
_notifyListenersSafely();
}
void _notifyListenersSafely() {
final phase = SchedulerBinding.instance.schedulerPhase;
final canNotifyNow =
phase == SchedulerPhase.idle ||
phase == SchedulerPhase.postFrameCallbacks;
if (canNotifyNow) {
notifyListeners();
return;
}
if (_notifyScheduled) return;
_notifyScheduled = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
_notifyScheduled = false;
notifyListeners();
});
}
String _describeFrame(
+72
View File
@@ -0,0 +1,72 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../storage/prefs_manager.dart';
/// Client-side accessibility/UI service that exposes a persistent shared text scale
/// factor. No MeshCoreConnector/RoomServer or protocol interaction occurs, and the
/// value is saved locally via SharedPreferences so it can be reused in Markdown
/// viewers, log panels, or other text-heavy widgets without redundant network
/// dependencies.
///
/// Widgets should scope rebuilds using the snippet below so only the scaled text
/// is rebuilt instead of the entire chat list:
/// ```dart
/// context.select<ChatTextScaleService, double>(
/// (service) => service.scale,
/// )
/// ```
class ChatTextScaleService extends ChangeNotifier {
static const _prefKey = 'chat_text_scale';
static const double _minScale = 0.8;
static const double _maxScale = 1.8;
double _scale = 1.0;
Timer? _saveTimer;
double get scale => _scale;
Future<void> initialize() async {
final stored = PrefsManager.instance.getDouble(_prefKey);
if (stored != null) {
_scale = _clamp(stored);
}
}
void setScale(double value, {bool persistImmediately = false}) {
final next = _clamp(value);
if (next == _scale) return;
_scale = next;
notifyListeners();
if (persistImmediately) {
_commitScale();
} else {
_scheduleSave();
}
}
void reset() {
setScale(1.0, persistImmediately: true);
}
void persist() => _commitScale();
@override
void dispose() {
_saveTimer?.cancel();
super.dispose();
}
void _scheduleSave() {
_saveTimer?.cancel();
_saveTimer = Timer(const Duration(milliseconds: 250), _commitScale);
}
void _commitScale() {
_saveTimer?.cancel();
PrefsManager.instance.setDouble(_prefKey, _scale);
}
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
}
+446
View File
@@ -0,0 +1,446 @@
import 'dart:convert';
import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
typedef ElevationDataSource =
Future<List<double?>> Function(List<LatLng> points);
class LineOfSightSample {
final double distanceMeters;
final double terrainMeters;
final double lineHeightMeters;
final double refractedHeightMeters;
final double clearanceMeters;
const LineOfSightSample({
required this.distanceMeters,
required this.terrainMeters,
required this.lineHeightMeters,
required this.refractedHeightMeters,
required this.clearanceMeters,
});
}
class LineOfSightResult {
final bool hasData;
final bool isClear;
final double totalDistanceMeters;
final double maxObstructionMeters;
final double? firstObstructionDistanceMeters;
final List<LineOfSightSample> samples;
final String? errorMessage;
final double usedKFactor;
final double? frequencyMHz;
const LineOfSightResult({
required this.hasData,
required this.isClear,
required this.totalDistanceMeters,
required this.maxObstructionMeters,
required this.firstObstructionDistanceMeters,
required this.samples,
required this.usedKFactor,
this.frequencyMHz,
this.errorMessage,
});
const LineOfSightResult.error({
required this.totalDistanceMeters,
required this.errorMessage,
this.usedKFactor = 4.0 / 3.0,
this.frequencyMHz,
}) : hasData = false,
isClear = false,
maxObstructionMeters = 0,
firstObstructionDistanceMeters = null,
samples = const [];
}
class LineOfSightPathSegment {
final int index;
final LatLng start;
final LatLng end;
final LineOfSightResult result;
const LineOfSightPathSegment({
required this.index,
required this.start,
required this.end,
required this.result,
});
}
class LineOfSightPathResult {
final List<LineOfSightPathSegment> segments;
final int clearSegments;
final int blockedSegments;
final int unknownSegments;
const LineOfSightPathResult({
required this.segments,
required this.clearSegments,
required this.blockedSegments,
required this.unknownSegments,
});
}
class LineOfSightService {
static const String errorElevationUnavailable =
'los_error_elevation_unavailable';
static const String errorInvalidInput = 'los_error_invalid_input';
static const double _earthRadiusMeters = 6371000.0;
static const Distance _distance = Distance();
static const Duration _cacheTtl = Duration(hours: 24);
static const int _maxFetchAttempts = 4; // initial try + 3 retries
static const Duration _initialBackoff = Duration(milliseconds: 300);
static const double _baselineFrequencyMHz = 915.0;
static const double _baselineKFactor = 4.0 / 3.0;
static double get baselineFrequencyMHz => _baselineFrequencyMHz;
static double get baselineKFactor => _baselineKFactor;
final http.Client _httpClient;
final bool _ownsHttpClient;
final ElevationDataSource? _elevationDataSource;
final Map<String, _CachedElevation> _elevationCache = {};
LineOfSightService({
http.Client? httpClient,
ElevationDataSource? elevationDataSource,
}) : _httpClient = httpClient ?? http.Client(),
_ownsHttpClient = httpClient == null,
_elevationDataSource = elevationDataSource;
Future<LineOfSightPathResult> analyzePath(
List<LatLng> points, {
double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0,
}) async {
if (points.length < 2) {
return const LineOfSightPathResult(
segments: [],
clearSegments: 0,
blockedSegments: 0,
unknownSegments: 0,
);
}
final segments = <LineOfSightPathSegment>[];
var clearSegments = 0;
var blockedSegments = 0;
var unknownSegments = 0;
final kFactor = _kFactorForFrequency(frequencyMHz);
for (int i = 0; i < points.length - 1; i++) {
final result = await analyzeLink(
points[i],
points[i + 1],
startAntennaHeightMeters: startAntennaHeightMeters,
endAntennaHeightMeters: endAntennaHeightMeters,
kFactor: kFactor,
frequencyMHz: frequencyMHz,
obstructionToleranceMeters: obstructionToleranceMeters,
);
segments.add(
LineOfSightPathSegment(
index: i,
start: points[i],
end: points[i + 1],
result: result,
),
);
if (!result.hasData) {
unknownSegments++;
} else if (result.isClear) {
clearSegments++;
} else {
blockedSegments++;
}
}
return LineOfSightPathResult(
segments: segments,
clearSegments: clearSegments,
blockedSegments: blockedSegments,
unknownSegments: unknownSegments,
);
}
Future<LineOfSightResult> analyzeLink(
LatLng start,
LatLng end, {
double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5,
required double kFactor,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0,
}) async {
final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end);
if (totalDistanceMeters <= 1) {
return LineOfSightResult(
hasData: true,
isClear: true,
totalDistanceMeters: totalDistanceMeters,
maxObstructionMeters: 0,
firstObstructionDistanceMeters: null,
samples: const [],
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
final samplePoints = _buildSamplePoints(start, end, totalDistanceMeters);
final elevations = await _getElevations(samplePoints);
if (elevations.any((e) => e == null)) {
return LineOfSightResult.error(
totalDistanceMeters: totalDistanceMeters,
errorMessage: errorElevationUnavailable,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
return computeFromElevations(
points: samplePoints,
elevations: elevations.cast<double>(),
startAntennaHeightMeters: startAntennaHeightMeters,
endAntennaHeightMeters: endAntennaHeightMeters,
kFactor: kFactor,
frequencyMHz: frequencyMHz,
obstructionToleranceMeters: obstructionToleranceMeters,
);
}
static LineOfSightResult computeFromElevations({
required List<LatLng> points,
required List<double> elevations,
double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5,
required double kFactor,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0,
}) {
if (points.length < 2 || elevations.length != points.length) {
return LineOfSightResult.error(
totalDistanceMeters: 0,
errorMessage: errorInvalidInput,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
final totalDistanceMeters = _distance.as(
LengthUnit.Meter,
points.first,
points.last,
);
final effectiveEarthRadius = _earthRadiusMeters * kFactor;
final startLineHeight = elevations.first + startAntennaHeightMeters;
final endLineHeight = elevations.last + endAntennaHeightMeters;
var maxObstructionMeters = 0.0;
double? firstObstructionDistanceMeters;
final samples = <LineOfSightSample>[];
var isClear = true;
for (int i = 0; i < points.length; i++) {
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
final distanceFromStart = totalDistanceMeters * fraction;
final lineHeight =
startLineHeight + (endLineHeight - startLineHeight) * fraction;
final earthBulge =
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
(2 * effectiveEarthRadius);
final terrainHeight = elevations[i] + earthBulge;
final clearance = lineHeight - terrainHeight;
final unrefBulge =
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
(2 * _earthRadiusMeters);
final refractedHeight = lineHeight + (unrefBulge - earthBulge);
if (clearance < -obstructionToleranceMeters) {
isClear = false;
final obstruction = -clearance;
if (obstruction > maxObstructionMeters) {
maxObstructionMeters = obstruction;
}
firstObstructionDistanceMeters ??= distanceFromStart;
}
samples.add(
LineOfSightSample(
distanceMeters: distanceFromStart,
terrainMeters: terrainHeight,
lineHeightMeters: lineHeight,
refractedHeightMeters: refractedHeight,
clearanceMeters: clearance,
),
);
}
return LineOfSightResult(
hasData: true,
isClear: isClear,
totalDistanceMeters: totalDistanceMeters,
maxObstructionMeters: maxObstructionMeters,
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
samples: samples,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
static double _kFactorForFrequency(double? frequencyMHz) {
if (frequencyMHz == null) return _baselineKFactor;
final delta =
(frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz;
final adjustment = delta * 0.15;
final scaled = _baselineKFactor * (1 + adjustment);
return scaled.clamp(1.1, 1.6).toDouble();
}
List<LatLng> _buildSamplePoints(
LatLng start,
LatLng end,
double distanceMeters,
) {
final sampleCount = distanceMeters < 2000
? 21
: distanceMeters < 10000
? 41
: 81;
final points = <LatLng>[];
for (int i = 0; i < sampleCount; i++) {
final t = i / (sampleCount - 1);
points.add(
LatLng(
start.latitude + (end.latitude - start.latitude) * t,
start.longitude + (end.longitude - start.longitude) * t,
),
);
}
return points;
}
Future<List<double?>> _getElevations(List<LatLng> points) async {
final dataSource = _elevationDataSource;
if (dataSource != null) {
return dataSource(points);
}
final uncached = <int, LatLng>{};
final values = List<double?>.filled(points.length, null);
for (int i = 0; i < points.length; i++) {
final key = _cacheKey(points[i]);
final cached = _readCachedValue(key);
if (cached != null) {
values[i] = cached;
} else {
uncached[i] = points[i];
}
}
if (uncached.isEmpty) return values;
final latCsv = uncached.values
.map((p) => p.latitude.toStringAsFixed(6))
.join(',');
final lonCsv = uncached.values
.map((p) => p.longitude.toStringAsFixed(6))
.join(',');
final uri = Uri.parse(
'https://api.open-meteo.com/v1/elevation?latitude=$latCsv&longitude=$lonCsv',
);
final response = await _getWithBackoff(uri);
if (response.statusCode != 200) {
return values;
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) {
return values;
}
final elevations = decoded['elevation'];
if (elevations is! List) {
return values;
}
final indices = uncached.keys.toList();
for (int i = 0; i < min(indices.length, elevations.length); i++) {
final value = elevations[i];
if (value is! num) continue;
final index = indices[i];
final elevation = value.toDouble();
values[index] = elevation;
_elevationCache[_cacheKey(points[index])] = _CachedElevation(
value: elevation,
expiresAt: DateTime.now().add(_cacheTtl),
);
}
return values;
}
Future<http.Response> _getWithBackoff(Uri uri) async {
var attempt = 0;
Duration backoff = _initialBackoff;
while (true) {
attempt++;
try {
final response = await _httpClient.get(uri);
if (!_shouldRetryStatus(response.statusCode) ||
attempt >= _maxFetchAttempts) {
return response;
}
} catch (_) {
if (attempt >= _maxFetchAttempts) rethrow;
}
await Future.delayed(backoff);
backoff *= 2;
}
}
bool _shouldRetryStatus(int statusCode) {
return statusCode == 429 || statusCode >= 500;
}
double? _readCachedValue(String key) {
final cached = _elevationCache[key];
if (cached == null) return null;
if (DateTime.now().isAfter(cached.expiresAt)) {
_elevationCache.remove(key);
return null;
}
return cached.value;
}
String _cacheKey(LatLng point) {
return '${point.latitude.toStringAsFixed(5)},${point.longitude.toStringAsFixed(5)}';
}
void dispose() {
if (_ownsHttpClient) {
_httpClient.close();
}
}
}
class _CachedElevation {
final double value;
final DateTime expiresAt;
const _CachedElevation({required this.value, required this.expiresAt});
}
+11 -4
View File
@@ -234,7 +234,11 @@ class MessageRetryService extends ChangeNotifier {
}
}
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
bool updateMessageFromSent(
Uint8List ackHash,
int timeoutMs, {
bool allowQueueFallback = true,
}) {
final ackHashHex = ackHash
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
@@ -277,7 +281,7 @@ class MessageRetryService extends ChangeNotifier {
}
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
if (messageId == null) {
if (messageId == null && allowQueueFallback) {
_debugLogService?.warn(
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
tag: 'AckHash',
@@ -320,7 +324,7 @@ class MessageRetryService extends ChangeNotifier {
if (messageId == null || contact == null) {
debugPrint('No pending message found for ACK hash: $ackHashHex');
return;
return false;
}
// Store the mapping for future lookups (e.g., when ACK arrives)
@@ -339,7 +343,7 @@ class MessageRetryService extends ChangeNotifier {
'Message $messageId no longer pending for ACK hash: $ackHashHex',
);
_ackHashToMessageId.remove(ackHashHex);
return;
return false;
}
// Add this ACK hash to the list of expected ACKs for this message (for history)
@@ -389,8 +393,11 @@ class MessageRetryService extends ChangeNotifier {
_startTimeoutTimer(messageId, actualTimeout);
debugPrint('Updated message $messageId with ACK hash: $ackHashHex');
return true;
}
bool get hasPendingMessages => _pendingMessages.isNotEmpty;
void _startTimeoutTimer(String messageId, int timeoutMs) {
_timeoutTimers[messageId]?.cancel();
_timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () {
+55 -28
View File
@@ -58,16 +58,22 @@ class NotificationService {
requestBadgePermission: true,
requestSoundPermission: true,
);
const windowsSettings = WindowsInitializationSettings(
appName: 'MeshCore Open',
appUserModelId: 'org.meshcore.open.app',
guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86',
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
macOS: macSettings,
windows: windowsSettings,
);
try {
await _notifications.initialize(
initSettings,
settings: initSettings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
_isInitialized = true;
@@ -76,6 +82,13 @@ class NotificationService {
}
}
Future<bool> _ensureInitialized() async {
if (!_isInitialized) {
await initialize();
}
return _isInitialized;
}
Future<bool> requestPermissions() async {
if (!_isInitialized) {
await initialize();
@@ -114,9 +127,7 @@ class NotificationService {
String? contactId,
int? badgeCount,
}) async {
if (!_isInitialized) {
await initialize();
}
if (!await _ensureInitialized()) return;
final androidDetails = AndroidNotificationDetails(
'messages',
@@ -148,13 +159,17 @@ class NotificationService {
macOS: macDetails,
);
try {
await _notifications.show(
contactId?.hashCode ?? 0,
contactName,
message,
notificationDetails,
id: contactId?.hashCode ?? 0,
title: contactName,
body: message,
notificationDetails: notificationDetails,
payload: 'message:$contactId',
);
} catch (e) {
debugPrint('Failed to show message notification: $e');
}
}
Future<void> _showAdvertNotificationImpl({
@@ -162,9 +177,7 @@ class NotificationService {
required String contactType,
String? contactId,
}) async {
if (!_isInitialized) {
await initialize();
}
if (!await _ensureInitialized()) return;
const androidDetails = AndroidNotificationDetails(
'adverts',
@@ -193,13 +206,17 @@ class NotificationService {
macOS: macDetails,
);
try {
await _notifications.show(
contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
_l10n.notification_newTypeDiscovered(contactType),
contactName,
notificationDetails,
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
title: _l10n.notification_newTypeDiscovered(contactType),
body: contactName,
notificationDetails: notificationDetails,
payload: 'advert:$contactId',
);
} catch (e) {
debugPrint('Failed to show advert notification: $e');
}
}
Future<void> _showChannelMessageNotificationImpl({
@@ -208,9 +225,7 @@ class NotificationService {
int? channelIndex,
int? badgeCount,
}) async {
if (!_isInitialized) {
await initialize();
}
if (!await _ensureInitialized()) return;
final androidDetails = AndroidNotificationDetails(
'channel_messages',
@@ -247,13 +262,17 @@ class NotificationService {
? _l10n.notification_receivedNewMessage
: preview;
try {
await _notifications.show(
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
channelName,
body,
notificationDetails,
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
title: channelName,
body: body,
notificationDetails: notificationDetails,
payload: 'channel:$channelIndex',
);
} catch (e) {
debugPrint('Failed to show channel notification: $e');
}
}
/// Returns a privacy-safe identifier for debug logging.
@@ -285,7 +304,7 @@ class NotificationService {
}
Future<void> cancel(int id) async {
await _notifications.cancel(id);
await _notifications.cancel(id: id);
}
//
@@ -396,6 +415,7 @@ class NotificationService {
Future<void> _showNotificationImmediately(
_PendingNotification notification,
) async {
try {
switch (notification.type) {
case _NotificationType.message:
await _showMessageNotificationImpl(
@@ -421,10 +441,13 @@ class NotificationService {
);
break;
}
} catch (e) {
debugPrint('Failed to show immediate notification: $e');
}
}
Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
if (!_isInitialized) await initialize();
if (!await _ensureInitialized()) return;
// Group by type
final messages = batch
@@ -468,13 +491,17 @@ class NotificationService {
const notificationDetails = NotificationDetails(android: androidDetails);
try {
await _notifications.show(
'batch_summary'.hashCode,
_l10n.notification_activityTitle,
parts.join(', '),
notificationDetails,
id: 'batch_summary'.hashCode,
title: _l10n.notification_activityTitle,
body: parts.join(', '),
notificationDetails: notificationDetails,
payload: 'batch',
);
} catch (e) {
debugPrint('Failed to show batch summary notification: $e');
}
}
}
+2
View File
@@ -33,6 +33,7 @@ class ContactStore {
'publicKey': base64Encode(contact.publicKey),
'name': contact.name,
'type': contact.type,
'flags': contact.flags,
'pathLength': contact.pathLength,
'path': base64Encode(contact.path),
'pathOverride': contact.pathOverride,
@@ -53,6 +54,7 @@ class ContactStore {
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
name: json['name'] as String? ?? 'Unknown',
type: json['type'] as int? ?? 0,
flags: json['flags'] as int? ?? 0,
pathLength: json['pathLength'] as int? ?? -1,
path: json['path'] != null
? Uint8List.fromList(base64Decode(json['path'] as String))
+26
View File
@@ -0,0 +1,26 @@
typedef BatteryVoltageRange = ({int minMv, int maxMv});
BatteryVoltageRange batteryVoltageRange(String chemistry) {
switch (chemistry) {
case 'lifepo4':
return (minMv: 2600, maxMv: 3650);
case 'lipo':
return (minMv: 3000, maxMv: 4200);
case 'nmc':
default:
return (minMv: 3000, maxMv: 4200);
}
}
int estimateBatteryPercentFromMillivolts(int millivolts, String chemistry) {
final range = batteryVoltageRange(chemistry);
if (millivolts <= range.minMv) return 0;
if (millivolts >= range.maxMv) return 100;
return (((millivolts - range.minMv) * 100) / (range.maxMv - range.minMv))
.round();
}
int estimateBatteryPercentFromVolts(double volts, String chemistry) {
final millivolts = (volts * 1000).round();
return estimateBatteryPercentFromMillivolts(millivolts, chemistry);
}
+17
View File
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class AdaptiveAppBarTitle extends StatelessWidget {
final String text;
const AdaptiveAppBarTitle(this.text, {super.key});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => SizedBox(
width: constraints.maxWidth,
child: FittedBox(fit: BoxFit.scaleDown, child: Text(text, maxLines: 1)),
),
);
}
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/widgets/battery_indicator.dart';
import 'package:provider/provider.dart';
import 'snr_indicator.dart';
class AppBarTitle extends StatelessWidget {
final String title;
final Widget? leading;
final Widget? trailing;
const AppBarTitle(this.title, {this.leading, this.trailing, super.key});
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final selfName = connector.selfName;
return LayoutBuilder(
builder: (context, constraints) {
final availableWidth = constraints.hasBoundedWidth
? constraints.maxWidth
: MediaQuery.sizeOf(context).width;
final compact = availableWidth < 240;
final showSubtitle =
!compact && connector.isConnected && selfName != null;
final showBattery = availableWidth >= 60;
final showSnr = availableWidth >= 110;
final showIndicators = showBattery || showSnr;
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
leading ?? const SizedBox.shrink(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
if (showSubtitle)
Text(
'($selfName)',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
if (showIndicators) const SizedBox(width: 6),
if (showIndicators)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (showBattery) BatteryIndicator(connector: connector),
if (showSnr) SNRIndicator(connector: connector),
],
),
trailing ?? const SizedBox.shrink(),
],
);
},
);
}
}
+7 -3
View File
@@ -66,10 +66,13 @@ class _BatteryIndicatorState extends State<BatteryIndicator> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(batteryUi.icon, size: 18, color: batteryUi.color),
const SizedBox(width: 2),
const SizedBox(height: 2),
Flexible(
child: Text(
displayText,
@@ -78,13 +81,14 @@ class _BatteryIndicatorState extends State<BatteryIndicator> {
fontWeight: FontWeight.w500,
color: batteryUi.color,
),
overflow: TextOverflow.visible,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
);
}
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/chat_text_scale_service.dart';
/// Gesture wrapper that exposes two-finger pinch-to-zoom for chat scrollables.
/// Double-tap resets the scale. Only the wrapper itself listens to gestures;
/// child scrollables keep their normal touch handling.
class ChatZoomWrapper extends StatefulWidget {
const ChatZoomWrapper({super.key, required this.child, this.onDoubleTap});
final Widget child;
final VoidCallback? onDoubleTap;
@override
State<ChatZoomWrapper> createState() => _ChatZoomWrapperState();
}
class _ChatZoomWrapperState extends State<ChatZoomWrapper> {
double? _startScale;
@override
Widget build(BuildContext context) {
final service = context.read<ChatTextScaleService>();
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTap: () {
service.reset();
service.persist();
widget.onDoubleTap?.call();
},
onScaleStart: (details) {
if (details.pointerCount != 2) return;
_startScale = service.scale;
},
onScaleUpdate: (details) {
if (details.pointerCount != 2) return;
final baseScale = _startScale ?? service.scale;
service.setScale(baseScale * details.scale);
},
onScaleEnd: (_) {
_startScale = null;
service.persist();
},
child: widget.child,
);
}
}
+15 -6
View File
@@ -3,7 +3,7 @@ import '../l10n/l10n.dart';
enum ContactSortOption { lastSeen, recentMessages, name }
enum ContactTypeFilter { all, users, repeaters, rooms }
enum ContactTypeFilter { all, favorites, users, repeaters, rooms }
class SortFilterMenuOption {
final int value;
@@ -94,11 +94,12 @@ const int _actionSortRecentMessages = 1;
const int _actionSortName = 2;
const int _actionSortLastSeen = 3;
const int _actionFilterAll = 4;
const int _actionFilterUsers = 5;
const int _actionFilterRepeaters = 6;
const int _actionFilterRooms = 7;
const int _actionToggleUnreadOnly = 8;
const int _actionNewGroup = 9;
const int _actionFilterFavorites = 5;
const int _actionFilterUsers = 6;
const int _actionFilterRepeaters = 7;
const int _actionFilterRooms = 8;
const int _actionToggleUnreadOnly = 9;
const int _actionNewGroup = 10;
class ContactsFilterMenu extends StatelessWidget {
final ContactSortOption sortOption;
@@ -154,6 +155,11 @@ class ContactsFilterMenu extends StatelessWidget {
label: l10n.listFilter_all,
checked: typeFilter == ContactTypeFilter.all,
),
SortFilterMenuOption(
value: _actionFilterFavorites,
label: l10n.listFilter_favorites,
checked: typeFilter == ContactTypeFilter.favorites,
),
SortFilterMenuOption(
value: _actionFilterUsers,
label: l10n.listFilter_users,
@@ -198,6 +204,9 @@ class ContactsFilterMenu extends StatelessWidget {
case _actionFilterUsers:
onTypeFilterChanged(ContactTypeFilter.users);
break;
case _actionFilterFavorites:
onTypeFilterChanged(ContactTypeFilter.favorites);
break;
case _actionFilterRepeaters:
onTypeFilterChanged(ContactTypeFilter.repeaters);
break;
+36
View File
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class MessageStatusIcon extends StatelessWidget {
final bool isAcked;
final bool isFailed;
final double size;
const MessageStatusIcon({
super.key,
required this.isAcked,
this.isFailed = false,
this.size = 14,
});
@override
Widget build(BuildContext context) {
if (isFailed) {
return Icon(Icons.cancel, size: size, color: Colors.red);
}
final Color color;
if (isAcked) {
color = Colors.green;
} else {
color = Colors.grey;
}
return SvgPicture.asset(
'assets/icons/done_all.svg',
width: size,
height: size,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
);
}
}
+89 -9
View File
@@ -1,7 +1,9 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/models/path_history.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
@@ -19,15 +21,22 @@ class PathManagementDialog {
}
}
class _PathManagementDialog extends StatelessWidget {
class _PathManagementDialog extends StatefulWidget {
final Contact contact;
const _PathManagementDialog({required this.contact});
@override
State<_PathManagementDialog> createState() => _PathManagementDialogState();
}
class _PathManagementDialogState extends State<_PathManagementDialog> {
bool _showAllPaths = false;
Contact _resolveContact(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
orElse: () => contact,
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
orElse: () => widget.contact,
);
}
@@ -134,6 +143,59 @@ class _PathManagementDialog extends StatelessWidget {
final currentContact = _resolveContact(connector);
final paths = pathService.getRecentPaths(currentContact.publicKeyHex);
final repeatersList = List.of(connector.directRepeaters)
..sort((a, b) => b.ranking.compareTo(a.ranking));
if (repeatersList.isEmpty) {
_showAllPaths = true;
}
final directRepeater = repeatersList.isEmpty
? null
: repeatersList.first;
final secondDirectRepeater = repeatersList.length < 2
? null
: repeatersList.elementAt(1);
final thirdDirectRepeater = repeatersList.length < 3
? null
: repeatersList.elementAt(2);
List<MapEntry<int, MapEntry<Color, PathRecord>>> pathsWithRepeaters =
paths.map((path) {
final isDirectRepeater =
directRepeater != null &&
path.pathBytes.isNotEmpty &&
directRepeater.pubkeyFirstByte == path.pathBytes.first;
final isSecondDirectRepeater =
secondDirectRepeater != null &&
path.pathBytes.isNotEmpty &&
secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
final isThirdDirectRepeater =
thirdDirectRepeater != null &&
path.pathBytes.isNotEmpty &&
thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
int ranking = -1;
Color color = Colors.grey;
if (isDirectRepeater) {
color = Colors.green;
ranking = 3;
} else if (isSecondDirectRepeater) {
color = Colors.yellow;
ranking = 2;
} else if (isThirdDirectRepeater) {
color = Colors.red;
ranking = 1;
} else if (path.wasFloodDiscovery) {
color = Colors.blue;
ranking = 0;
}
return MapEntry(ranking, MapEntry(color, path));
}).toList();
pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key));
return AlertDialog(
title: Text(l10n.chat_pathManagement),
content: SingleChildScrollView(
@@ -147,6 +209,17 @@ class _PathManagementDialog extends StatelessWidget {
),
const SizedBox(height: 12),
if (paths.isNotEmpty) ...[
if (repeatersList.isNotEmpty)
FeatureToggleRow(
title: l10n.chat_ShowAllPaths,
subtitle: "",
value: _showAllPaths,
onChanged: (val) {
setState(() {
_showAllPaths = val;
});
},
),
Text(
l10n.chat_recentAckPaths,
style: const TextStyle(
@@ -154,7 +227,7 @@ class _PathManagementDialog extends StatelessWidget {
fontSize: 12,
),
),
if (paths.length >= 100) ...[
if (pathsWithRepeaters.length >= 100) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
@@ -173,16 +246,20 @@ class _PathManagementDialog extends StatelessWidget {
),
],
const SizedBox(height: 8),
...paths.map((path) {
...pathsWithRepeaters.map((entry) {
final path = entry.value.value;
final color = entry.value.key;
if (!_showAllPaths && entry.key < 1) {
return const SizedBox.shrink();
} else {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
dense: true,
leading: CircleAvatar(
radius: 16,
backgroundColor: path.wasFloodDiscovery
? Colors.blue
: Colors.green,
backgroundColor: color,
child: Text(
'${path.hopCount}',
style: const TextStyle(fontSize: 12),
@@ -237,7 +314,9 @@ class _PathManagementDialog extends StatelessWidget {
return;
}
final pathBytes = Uint8List.fromList(path.pathBytes);
final pathBytes = Uint8List.fromList(
path.pathBytes,
);
final pathLength = path.pathBytes.length;
await connector.setPathOverride(
@@ -259,6 +338,7 @@ class _PathManagementDialog extends StatelessWidget {
},
),
);
}
}),
const Divider(),
] else ...[
+1 -1
View File
@@ -156,7 +156,7 @@ class _QrScannerWidgetState extends State<QrScannerWidget>
MobileScanner(
controller: _controller,
onDetect: _handleDetection,
errorBuilder: (context, error, child) {
errorBuilder: (context, error) {
return _buildErrorWidget(context, error);
},
),
+157 -14
View File
@@ -1,4 +1,13 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
class SNRUi {
final IconData icon;
final Color color;
final String text;
const SNRUi(this.icon, this.color, this.text);
}
List<double> getSNRfromSF(int spreadingFactor) {
switch (spreadingFactor) {
@@ -19,20 +28,19 @@ List<double> getSNRfromSF(int spreadingFactor) {
}
}
class SNRIcon extends StatelessWidget {
final double snr;
final List<double> snrLevels;
SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) {
if (snr == null ||
spreadingFactor == null ||
spreadingFactor < 7 ||
spreadingFactor > 12) {
return const SNRUi(Icons.signal_cellular_off, Colors.grey, '');
}
const SNRIcon({
super.key,
required this.snr,
this.snrLevels = const [4.0, -2.0, -4.0, -6.0],
});
final snrLevels = getSNRfromSF(spreadingFactor);
@override
Widget build(BuildContext context) {
IconData icon;
Color color;
String text = '${snr.toStringAsFixed(1)} dB';
if (snr >= snrLevels[0]) {
icon = Icons.signal_cellular_alt;
@@ -51,12 +59,147 @@ class SNRIcon extends StatelessWidget {
color = Colors.red;
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
return SNRUi(icon, color, text);
}
class SNRIndicator extends StatefulWidget {
final MeshCoreConnector connector;
const SNRIndicator({super.key, required this.connector});
@override
State<SNRIndicator> createState() => _SNRIndicatorState();
}
class _SNRIndicatorState extends State<SNRIndicator> {
@override
Widget build(BuildContext context) {
final directRepeaters = widget.connector.directRepeaters;
final directBestRepeaters = List.of(directRepeaters)
..sort((a, b) => (b.ranking).compareTo(a.ranking));
final directRepeater = directBestRepeaters.isEmpty
? null
: directBestRepeaters.first;
final snrUi = snrUiFromSNR(
directBestRepeaters.isNotEmpty ? directRepeater!.snr : null,
widget.connector.currentSf,
);
return InkWell(
onTap: () {
if (directRepeater != null) {
_showFullPathDialog(context, directBestRepeaters);
}
},
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color),
Text('$snr dB', style: TextStyle(fontSize: 10, color: color)),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(snrUi.icon, size: 18, color: snrUi.color),
Text(
snrUi.text,
style: TextStyle(fontSize: 12, color: snrUi.color),
),
],
),
if (directRepeater != null)
Text(
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
String _formatLastUpdated(DateTime lastSeen) {
final now = DateTime.now();
final diff = now.difference(lastSeen);
if (diff.isNegative) {
return "0s";
}
if (diff.inMinutes < 1) {
return "${diff.inSeconds}s";
}
if (diff.inMinutes < 60) {
return "${diff.inMinutes}m";
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return "${hours}h";
}
final days = diff.inDays;
return "${days}d";
}
void _showFullPathDialog(
BuildContext context,
List<DirectRepeater> directBestRepeaters,
) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.snrIndicator_nearByRepeaters),
content: SizedBox(
child: Scrollbar(
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: directBestRepeaters.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final repeater = directBestRepeaters[index];
final snrUi = snrUiFromSNR(
repeater.snr,
widget.connector.currentSf,
);
final name = widget.connector.contacts
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
.map((c) => c.name)
.firstOrNull;
return Column(
children: [
ListTile(
leading: Icon(snrUi.icon, color: snrUi.color),
title: Text(
name ??
repeater.pubkeyFirstByte
.toRadixString(16)
.padLeft(2, '0'),
),
subtitle: Text(
'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}',
),
),
],
);
},
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_close),
),
],
),
);
}
}
+12 -5
View File
@@ -5,10 +5,13 @@ PODS:
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- mobile_scanner (6.0.2):
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -24,8 +27,9 @@ DEPENDENCIES:
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
@@ -39,9 +43,11 @@ EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
@@ -53,10 +59,11 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
-1095
View File
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 5.0.0+6
version: 6.0.0+7
environment:
sdk: ^3.9.2
@@ -41,25 +41,28 @@ dependencies:
provider: ^6.1.5+1
shared_preferences: ^2.2.2
uuid: ^4.3.3
flutter_map: ^7.0.2
flutter_map: ^8.2.2
latlong2: ^0.9.1
flutter_local_notifications: ^18.0.1
flutter_local_notifications: ^20.1.0
crypto: ^3.0.3
pointycastle: ^3.7.4
pointycastle: ^4.0.0
http: ^1.2.0
cached_network_image: ^3.4.1
flutter_cache_manager: ^3.4.1
flutter_foreground_task: ^6.1.2
wakelock_plus: ^1.2.8
flutter_foreground_task: ^9.2.0
wakelock_plus: ^1.4.0
characters: ^1.4.0
package_info_plus: ^8.0.0
mobile_scanner: ^6.0.0 # QR/barcode scanning
package_info_plus: ^9.0.0
mobile_scanner: ^7.1.4 # QR/barcode scanning
qr_flutter: ^4.1.0 # QR code generation
url_launcher: ^6.3.0 # Launch URLs in system browser
flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text
gpx: ^2.3.0
path_provider: ^2.1.5
share_plus: ^12.0.1
material_symbols_icons: ^4.2906.0
web: ^1.1.1
flutter_svg: ^2.0.10+1
dev_dependencies:
flutter_test:
@@ -70,8 +73,8 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
flutter_launcher_icons: ^0.13.1
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -87,6 +90,7 @@ flutter:
assets:
- assets/images/
- assets/icons/
flutter_launcher_icons:
android: true
@@ -0,0 +1,74 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/services/line_of_sight_service.dart';
void main() {
List<LatLng> makePoints(int count) {
return List<LatLng>.generate(count, (i) => LatLng(0, i * 0.00001));
}
test('computeFromElevations reports clear LOS on flat terrain', () {
final points = makePoints(21);
final elevations = List<double>.filled(points.length, 100);
final result = LineOfSightService.computeFromElevations(
points: points,
elevations: elevations,
startAntennaHeightMeters: 2,
endAntennaHeightMeters: 2,
kFactor: 4.0 / 3.0,
);
expect(result.hasData, isTrue);
expect(result.isClear, isTrue);
expect(result.maxObstructionMeters, equals(0));
expect(result.firstObstructionDistanceMeters, isNull);
});
test(
'computeFromElevations reports blocked LOS with central obstruction',
() {
final points = makePoints(21);
final elevations = List<double>.filled(points.length, 100);
elevations[10] = 300;
final result = LineOfSightService.computeFromElevations(
points: points,
elevations: elevations,
startAntennaHeightMeters: 1.5,
endAntennaHeightMeters: 1.5,
kFactor: 4.0 / 3.0,
);
expect(result.hasData, isTrue);
expect(result.isClear, isFalse);
expect(result.maxObstructionMeters, greaterThan(0));
expect(result.firstObstructionDistanceMeters, isNotNull);
},
);
test('analyzePath summarizes clear and blocked segments', () async {
final service = LineOfSightService(
elevationDataSource: (points) async {
final elevations = List<double?>.filled(points.length, 100);
if (points.first.longitude > 0.00005) {
elevations[elevations.length ~/ 2] = 300;
}
return elevations;
},
);
final path = [
const LatLng(0, 0),
const LatLng(0, 0.0001),
const LatLng(0, 0.0002),
];
final result = await service.analyzePath(path);
expect(result.segments.length, 2);
expect(result.clearSegments, 1);
expect(result.blockedSegments, 1);
expect(result.unknownSegments, 0);
});
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/utils/battery_utils.dart';
void main() {
group('battery utils', () {
test('nmc range maps 3.0V to 0% and 4.2V to 100%', () {
expect(estimateBatteryPercentFromVolts(3.0, 'nmc'), 0);
expect(estimateBatteryPercentFromVolts(4.2, 'nmc'), 100);
});
test('lifepo4 range maps 2.6V to 0% and 3.65V to 100%', () {
expect(estimateBatteryPercentFromVolts(2.6, 'lifepo4'), 0);
expect(estimateBatteryPercentFromVolts(3.65, 'lifepo4'), 100);
});
test('unknown chemistry falls back to nmc mapping', () {
expect(
estimateBatteryPercentFromMillivolts(3600, 'unknown'),
estimateBatteryPercentFromMillivolts(3600, 'nmc'),
);
});
});
}
+1 -127
View File
@@ -1,127 +1 @@
{
"bg": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"de": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"es": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"fr": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"it": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"nl": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"pl": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"pt": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"ru": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"sk": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"sl": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"sv": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"uk": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
],
"zh": [
"notification_activityTitle",
"notification_messagesCount",
"notification_channelMessagesCount",
"notification_newNodesCount",
"notification_newTypeDiscovered",
"notification_receivedNewMessage"
]
}
{}
+1
View File
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)